diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2020-01-24 11:08:44 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-24 11:08:44 -0600 |
commit | 4460bf40c9e935acb853b5d61279a50014ede0b3 (patch) | |
tree | b847c846e79c815c329ea6e7b843bdbf6ef82c86 | |
parent | 9bcf10e97e9b7c8fc12961e3950ce0a8e64231ce (diff) | |
parent | e9cef7c4b16b2cb572ac19fcd39217f7934cfa99 (diff) | |
download | libgit2-4460bf40c9e935acb853b5d61279a50014ede0b3.tar.gz |
Merge pull request #5286 from libgit2/ethomson/gssapi
HTTP: Support Apache-based servers with Negotiate
-rw-r--r-- | azure-pipelines.yml | 17 | ||||
-rw-r--r-- | azure-pipelines/docker/xenial | 4 | ||||
-rwxr-xr-x | azure-pipelines/test.sh | 74 | ||||
-rw-r--r-- | cmake/Modules/SelectGSSAPI.cmake | 2 | ||||
-rw-r--r-- | include/git2/common.h | 8 | ||||
-rw-r--r-- | include/git2/errors.h | 3 | ||||
-rw-r--r-- | src/buffer.c | 5 | ||||
-rw-r--r-- | src/buffer.h | 1 | ||||
-rw-r--r-- | src/net.c | 227 | ||||
-rw-r--r-- | src/net.h | 29 | ||||
-rw-r--r-- | src/netops.c | 98 | ||||
-rw-r--r-- | src/netops.h | 11 | ||||
-rw-r--r-- | src/settings.c | 5 | ||||
-rw-r--r-- | src/trace.h | 2 | ||||
-rw-r--r-- | src/transports/auth_negotiate.c | 59 | ||||
-rw-r--r-- | src/transports/auth_ntlm.h | 3 | ||||
-rw-r--r-- | src/transports/http.c | 1828 | ||||
-rw-r--r-- | src/transports/http.h | 3 | ||||
-rw-r--r-- | src/transports/httpclient.c | 1526 | ||||
-rw-r--r-- | src/transports/httpclient.h | 190 | ||||
-rw-r--r-- | src/transports/smart_protocol.c | 8 | ||||
-rw-r--r-- | src/transports/winhttp.c | 58 | ||||
-rw-r--r-- | tests/CMakeLists.txt | 14 | ||||
-rw-r--r-- | tests/clar_libgit2_trace.c | 15 | ||||
-rw-r--r-- | tests/network/joinpath.c | 194 | ||||
-rw-r--r-- | tests/network/redirect.c | 20 | ||||
-rw-r--r-- | tests/online/clone.c | 10 | ||||
-rw-r--r-- | tests/online/push.c | 8 |
28 files changed, 2846 insertions, 1576 deletions
diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5ee741ce0..b5271d083 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -19,7 +19,8 @@ jobs: environmentVariables: | CC=gcc CMAKE_GENERATOR=Ninja - CMAKE_OPTIONS=-DUSE_HTTPS=OpenSSL -DREGEX_BACKEND=builtin -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DVALGRIND=on + CMAKE_OPTIONS=-DUSE_HTTPS=OpenSSL -DREGEX_BACKEND=builtin -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DVALGRIND=on -DUSE_GSSAPI=ON + GITTEST_NEGOTIATE_PASSWORD=$(GITTEST_NEGOTIATE_PASSWORD) - job: linux_amd64_xenial_gcc_mbedtls displayName: 'Linux (amd64; Xenial; GCC; mbedTLS)' @@ -34,7 +35,8 @@ jobs: environmentVariables: | CC=gcc CMAKE_GENERATOR=Ninja - CMAKE_OPTIONS=-DUSE_HTTPS=mbedTLS -DUSE_SHA1=HTTPS -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DVALGRIND=on + CMAKE_OPTIONS=-DUSE_HTTPS=mbedTLS -DUSE_SHA1=HTTPS -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DVALGRIND=on -DUSE_GSSAPI=ON + GITTEST_NEGOTIATE_PASSWORD=$(GITTEST_NEGOTIATE_PASSWORD) - job: linux_amd64_xenial_clang_openssl displayName: 'Linux (amd64; Xenial; Clang; OpenSSL)' @@ -49,7 +51,8 @@ jobs: environmentVariables: | CC=clang CMAKE_GENERATOR=Ninja - CMAKE_OPTIONS=-DUSE_HTTPS=OpenSSL -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DVALGRIND=on + CMAKE_OPTIONS=-DUSE_HTTPS=OpenSSL -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DVALGRIND=on -DUSE_GSSAPI=ON + GITTEST_NEGOTIATE_PASSWORD=$(GITTEST_NEGOTIATE_PASSWORD) - job: linux_amd64_xenial_clang_mbedtls displayName: 'Linux (amd64; Xenial; Clang; mbedTLS)' @@ -64,7 +67,8 @@ jobs: environmentVariables: | CC=clang CMAKE_GENERATOR=Ninja - CMAKE_OPTIONS=-DUSE_HTTPS=mbedTLS -DUSE_SHA1=HTTPS -DREGEX_BACKEND=pcre -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DVALGRIND=on + CMAKE_OPTIONS=-DUSE_HTTPS=mbedTLS -DUSE_SHA1=HTTPS -DREGEX_BACKEND=pcre -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=valgrind -DVALGRIND=on -DUSE_GSSAPI=ON + GITTEST_NEGOTIATE_PASSWORD=$(GITTEST_NEGOTIATE_PASSWORD) - job: macos displayName: 'macOS' @@ -81,6 +85,7 @@ jobs: CMAKE_GENERATOR: Ninja CMAKE_OPTIONS: -DREGEX_BACKEND=regcomp_l -DDEPRECATE_HARD=ON -DUSE_LEAK_CHECKER=leaks -DUSE_GSSAPI=ON SKIP_SSH_TESTS: true + GITTEST_NEGOTIATE_PASSWORD: $(GITTEST_NEGOTIATE_PASSWORD) - job: windows_vs_amd64 displayName: 'Windows (amd64; Visual Studio)' @@ -92,6 +97,7 @@ jobs: CMAKE_GENERATOR: Visual Studio 12 2013 Win64 CMAKE_OPTIONS: -DMSVC_CRTDBG=ON -DDEPRECATE_HARD=ON SKIP_SSH_TESTS: true + SKIP_NEGOTIATE_TESTS: true - job: windows_vs_x86 displayName: 'Windows (x86; Visual Studio)' @@ -103,6 +109,7 @@ jobs: CMAKE_GENERATOR: Visual Studio 12 2013 CMAKE_OPTIONS: -DMSVC_CRTDBG=ON -DDEPRECATE_HARD=ON -DUSE_SHA1=HTTPS SKIP_SSH_TESTS: true + SKIP_NEGOTIATE_TESTS: true - job: windows_mingw_amd64 displayName: 'Windows (amd64; MinGW)' @@ -120,6 +127,7 @@ jobs: CMAKE_GENERATOR: MinGW Makefiles CMAKE_OPTIONS: -DDEPRECATE_HARD=ON SKIP_SSH_TESTS: true + SKIP_NEGOTIATE_TESTS: true - job: windows_mingw_x86 displayName: 'Windows (x86; MinGW)' @@ -138,6 +146,7 @@ jobs: CMAKE_GENERATOR: MinGW Makefiles CMAKE_OPTIONS: -DDEPRECATE_HARD=ON SKIP_SSH_TESTS: true + SKIP_NEGOTIATE_TESTS: true - job: documentation displayName: 'Generate Documentation' diff --git a/azure-pipelines/docker/xenial b/azure-pipelines/docker/xenial index 19b9fab81..6e3a469ca 100644 --- a/azure-pipelines/docker/xenial +++ b/azure-pipelines/docker/xenial @@ -1,7 +1,7 @@ ARG BASE FROM $BASE AS apt RUN apt-get update && \ - apt-get install -y --no-install-recommends \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ bzip2 \ clang \ cmake \ @@ -9,8 +9,10 @@ RUN apt-get update && \ gcc \ git \ gosu \ + krb5-user \ libcurl4-gnutls-dev \ libgcrypt20-dev \ + libkrb5-dev \ libpcre3-dev \ libssl-dev \ libz-dev \ diff --git a/azure-pipelines/test.sh b/azure-pipelines/test.sh index 56d8264d9..39e0b885b 100755 --- a/azure-pipelines/test.sh +++ b/azure-pipelines/test.sh @@ -6,6 +6,11 @@ if [ -n "$SKIP_TESTS" ]; then exit 0 fi +# Windows doesn't run the NTLM tests properly (yet) +if [[ "$(uname -s)" == MINGW* ]]; then + SKIP_NTLM_TESTS=1 +fi + SOURCE_DIR=${SOURCE_DIR:-$( cd "$( dirname "${BASH_SOURCE[0]}" )" && dirname $( pwd ) )} BUILD_DIR=$(pwd) TMPDIR=${TMPDIR:-/tmp} @@ -89,6 +94,16 @@ if [ -z "$SKIP_PROXY_TESTS" ]; then java -jar poxyproxy.jar --address 127.0.0.1 --port 8090 --credentials foo:bar --auth-type ntlm --quiet & fi +if [ -z "$SKIP_NTLM_TESTS" ]; then + curl -L https://github.com/ethomson/poxygit/releases/download/v0.4.0/poxygit-0.4.0.jar >poxygit.jar + + echo "" + echo "Starting HTTP server..." + NTLM_DIR=`mktemp -d ${TMPDIR}/ntlm.XXXXXXXX` + git init --bare "${NTLM_DIR}/test.git" + java -jar poxygit.jar --address 127.0.0.1 --port 9000 --credentials foo:baz --quiet "${NTLM_DIR}" & +fi + if [ -z "$SKIP_SSH_TESTS" ]; then echo "Starting ssh daemon..." HOME=`mktemp -d ${TMPDIR}/home.XXXXXXXX` @@ -207,6 +222,65 @@ if [ -z "$SKIP_PROXY_TESTS" ]; then unset GITTEST_REMOTE_PROXY_PASS fi +if [ -z "$SKIP_NTLM_TESTS" ]; then + echo "" + echo "Running NTLM tests (IIS emulation)" + echo "" + + export GITTEST_REMOTE_URL="http://localhost:9000/ntlm/test.git" + export GITTEST_REMOTE_USER="foo" + export GITTEST_REMOTE_PASS="baz" + run_test auth_clone_and_push + unset GITTEST_REMOTE_URL + unset GITTEST_REMOTE_USER + unset GITTEST_REMOTE_PASS + + echo "" + echo "Running NTLM tests (Apache emulation)" + echo "" + + export GITTEST_REMOTE_URL="http://localhost:9000/broken-ntlm/test.git" + export GITTEST_REMOTE_USER="foo" + export GITTEST_REMOTE_PASS="baz" + run_test auth_clone_and_push + unset GITTEST_REMOTE_URL + unset GITTEST_REMOTE_USER + unset GITTEST_REMOTE_PASS +fi + +if [ -z "$SKIP_NEGOTIATE_TESTS" -a -n "$GITTEST_NEGOTIATE_PASSWORD" ]; then + echo "" + echo "Running SPNEGO tests" + echo "" + + if [ "$(uname -s)" = "Darwin" ]; then + KINIT_FLAGS="--password-file=STDIN" + fi + + echo $GITTEST_NEGOTIATE_PASSWORD | kinit $KINIT_FLAGS test@LIBGIT2.ORG + klist -5f + + export GITTEST_REMOTE_URL="https://test.libgit2.org/kerberos/empty.git" + export GITTEST_REMOTE_DEFAULT="true" + run_test auth_clone + unset GITTEST_REMOTE_URL + unset GITTEST_REMOTE_DEFAULT + + echo "" + echo "Running SPNEGO tests (expect/continue)" + echo "" + + export GITTEST_REMOTE_URL="https://test.libgit2.org/kerberos/empty.git" + export GITTEST_REMOTE_DEFAULT="true" + export GITTEST_REMOTE_EXPECTCONTINUE="true" + run_test auth_clone + unset GITTEST_REMOTE_URL + unset GITTEST_REMOTE_DEFAULT + unset GITTEST_REMOTE_EXPECTCONTINUE + + kdestroy -A +fi + if [ -z "$SKIP_SSH_TESTS" ]; then echo "" echo "Running ssh tests" diff --git a/cmake/Modules/SelectGSSAPI.cmake b/cmake/Modules/SelectGSSAPI.cmake index 41f837587..857c449e7 100644 --- a/cmake/Modules/SelectGSSAPI.cmake +++ b/cmake/Modules/SelectGSSAPI.cmake @@ -49,5 +49,5 @@ IF(GSS_BACKEND) ENDIF() ELSE() SET(GIT_GSSAPI 0) - ADD_FEATURE_INFO(SPNEGO NO "") + ADD_FEATURE_INFO(SPNEGO NO "SPNEGO authentication support") ENDIF() diff --git a/include/git2/common.h b/include/git2/common.h index 438198295..947e40845 100644 --- a/include/git2/common.h +++ b/include/git2/common.h @@ -203,7 +203,8 @@ typedef enum { GIT_OPT_ENABLE_UNSAVED_INDEX_SAFETY, GIT_OPT_GET_PACK_MAX_OBJECTS, GIT_OPT_SET_PACK_MAX_OBJECTS, - GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS + GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, + GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE } git_libgit2_opt_t; /** @@ -397,6 +398,11 @@ typedef enum { * > This will cause .keep file existence checks to be skipped when * > accessing packfiles, which can help performance with remote filesystems. * + * opts(GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, int enabled) + * > When connecting to a server using NTLM or Negotiate + * > authentication, use expect/continue when POSTing data. + * > This option is not available on Windows. + * * @param option Option key * @param ... value to set the option * @return 0 on success, <0 on failure diff --git a/include/git2/errors.h b/include/git2/errors.h index 4e19f8925..370b6ac49 100644 --- a/include/git2/errors.h +++ b/include/git2/errors.h @@ -106,7 +106,8 @@ typedef enum { GIT_ERROR_FILESYSTEM, GIT_ERROR_PATCH, GIT_ERROR_WORKTREE, - GIT_ERROR_SHA1 + GIT_ERROR_SHA1, + GIT_ERROR_HTTP } git_error_t; /** diff --git a/src/buffer.c b/src/buffer.c index 61cf9675b..328fdfe7f 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -567,6 +567,11 @@ void git_buf_copy_cstr(char *data, size_t datasize, const git_buf *buf) data[copylen] = '\0'; } +void git_buf_consume_bytes(git_buf *buf, size_t len) +{ + git_buf_consume(buf, buf->ptr + len); +} + void git_buf_consume(git_buf *buf, const char *end) { if (end > buf->ptr && end <= buf->ptr + buf->size) { diff --git a/src/buffer.h b/src/buffer.h index 7910b6338..6b717d2e9 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -113,6 +113,7 @@ int git_buf_puts(git_buf *buf, const char *string); int git_buf_printf(git_buf *buf, const char *format, ...) GIT_FORMAT_PRINTF(2, 3); int git_buf_vprintf(git_buf *buf, const char *format, va_list ap); void git_buf_clear(git_buf *buf); +void git_buf_consume_bytes(git_buf *buf, size_t len); void git_buf_consume(git_buf *buf, const char *end); void git_buf_truncate(git_buf *buf, size_t len); void git_buf_shorten(git_buf *buf, size_t amount); @@ -153,6 +153,187 @@ done: return error; } +int git_net_url_joinpath( + git_net_url *out, + git_net_url *one, + const char *two) +{ + git_buf path = GIT_BUF_INIT; + const char *query; + size_t one_len, two_len; + + git_net_url_dispose(out); + + if ((query = strchr(two, '?')) != NULL) { + two_len = query - two; + + if (*(++query) != '\0') { + out->query = git__strdup(query); + GIT_ERROR_CHECK_ALLOC(out->query); + } + } else { + two_len = strlen(two); + } + + /* Strip all trailing `/`s from the first path */ + one_len = one->path ? strlen(one->path) : 0; + while (one_len && one->path[one_len - 1] == '/') + one_len--; + + /* Strip all leading `/`s from the second path */ + while (*two == '/') { + two++; + two_len--; + } + + git_buf_put(&path, one->path, one_len); + git_buf_putc(&path, '/'); + git_buf_put(&path, two, two_len); + + if (git_buf_oom(&path)) + return -1; + + out->path = git_buf_detach(&path); + + if (one->scheme) { + out->scheme = git__strdup(one->scheme); + GIT_ERROR_CHECK_ALLOC(out->scheme); + } + + if (one->host) { + out->host = git__strdup(one->host); + GIT_ERROR_CHECK_ALLOC(out->host); + } + + if (one->port) { + out->port = git__strdup(one->port); + GIT_ERROR_CHECK_ALLOC(out->port); + } + + if (one->username) { + out->username = git__strdup(one->username); + GIT_ERROR_CHECK_ALLOC(out->username); + } + + if (one->password) { + out->password = git__strdup(one->password); + GIT_ERROR_CHECK_ALLOC(out->password); + } + + return 0; +} + +/* + * Some servers strip the query parameters from the Location header + * when sending a redirect. Others leave it in place. + * Check for both, starting with the stripped case first, + * since it appears to be more common. + */ +static void remove_service_suffix( + git_net_url *url, + const char *service_suffix) +{ + const char *service_query = strchr(service_suffix, '?'); + size_t full_suffix_len = strlen(service_suffix); + size_t suffix_len = service_query ? + (size_t)(service_query - service_suffix) : full_suffix_len; + size_t path_len = strlen(url->path); + ssize_t truncate = -1; + + /* + * Check for a redirect without query parameters, + * like "/newloc/info/refs"' + */ + if (suffix_len && path_len >= suffix_len) { + size_t suffix_offset = path_len - suffix_len; + + if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 && + (!service_query || git__strcmp(url->query, service_query + 1) == 0)) { + truncate = suffix_offset; + } + } + + /* + * If we haven't already found where to truncate to remove the + * suffix, check for a redirect with query parameters, like + * "/newloc/info/refs?service=git-upload-pack" + */ + if (truncate < 0 && git__suffixcmp(url->path, service_suffix) == 0) + truncate = path_len - full_suffix_len; + + /* Ensure we leave a minimum of '/' as the path */ + if (truncate == 0) + truncate++; + + if (truncate > 0) { + url->path[truncate] = '\0'; + + git__free(url->query); + url->query = NULL; + } +} + +int git_net_url_apply_redirect( + git_net_url *url, + const char *redirect_location, + const char *service_suffix) +{ + git_net_url tmp = GIT_NET_URL_INIT; + int error = 0; + + assert(url && redirect_location); + + if (redirect_location[0] == '/') { + git__free(url->path); + + if ((url->path = git__strdup(redirect_location)) == NULL) { + error = -1; + goto done; + } + } else { + git_net_url *original = url; + + if ((error = git_net_url_parse(&tmp, redirect_location)) < 0) + goto done; + + /* Validate that this is a legal redirection */ + + 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); + + error = -1; + goto done; + } + + 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); + + error = -1; + goto done; + } + + git_net_url_swap(url, &tmp); + } + + /* Remove the service suffix if it was given to us */ + if (service_suffix) + remove_service_suffix(url, service_suffix); + +done: + git_net_url_dispose(&tmp); + return error; +} + +bool git_net_url_valid(git_net_url *url) +{ + return (url->host && url->port && url->path); +} + int git_net_url_is_default_port(git_net_url *url) { return (strcmp(url->port, default_port_for_scheme(url->scheme)) == 0); @@ -167,6 +348,51 @@ void git_net_url_swap(git_net_url *a, git_net_url *b) memcpy(b, &tmp, sizeof(git_net_url)); } +int git_net_url_fmt(git_buf *buf, git_net_url *url) +{ + git_buf_puts(buf, url->scheme); + git_buf_puts(buf, "://"); + + if (url->username) { + git_buf_puts(buf, url->username); + + if (url->password) { + git_buf_puts(buf, ":"); + git_buf_puts(buf, url->password); + } + + git_buf_putc(buf, '@'); + } + + git_buf_puts(buf, url->host); + + if (url->port && !git_net_url_is_default_port(url)) { + git_buf_putc(buf, ':'); + git_buf_puts(buf, url->port); + } + + git_buf_puts(buf, url->path ? url->path : "/"); + + if (url->query) { + git_buf_putc(buf, '?'); + git_buf_puts(buf, url->query); + } + + return git_buf_oom(buf) ? -1 : 0; +} + +int git_net_url_fmt_path(git_buf *buf, git_net_url *url) +{ + git_buf_puts(buf, url->path ? url->path : "/"); + + if (url->query) { + git_buf_putc(buf, '?'); + git_buf_puts(buf, url->query); + } + + return git_buf_oom(buf) ? -1 : 0; +} + void git_net_url_dispose(git_net_url *url) { if (url->username) @@ -179,6 +405,7 @@ void git_net_url_dispose(git_net_url *url) git__free(url->host); url->host = NULL; git__free(url->port); url->port = NULL; git__free(url->path); url->path = NULL; + git__free(url->query); url->query = NULL; git__free(url->username); url->username = NULL; git__free(url->password); url->password = NULL; } @@ -22,15 +22,36 @@ typedef struct 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); +extern int git_net_url_parse(git_net_url *url, const char *str); + +/** Appends a path and/or query string to the given URL */ +extern int git_net_url_joinpath( + git_net_url *out, + git_net_url *in, + const char *path); + +/** Ensures that a URL is minimally valid (contains a host, port and path) */ +extern bool git_net_url_valid(git_net_url *url); /** Returns nonzero if the URL is on the default port. */ -int git_net_url_is_default_port(git_net_url *url); +extern int git_net_url_is_default_port(git_net_url *url); + +/* Applies a redirect to the URL with a git-aware service suffix. */ +extern int git_net_url_apply_redirect( + git_net_url *url, + const char *redirect_location, + const char *service_suffix); /** Swaps the contents of one URL for another. */ -void git_net_url_swap(git_net_url *a, git_net_url *b); +extern void git_net_url_swap(git_net_url *a, git_net_url *b); + +/** Places the URL into the given buffer. */ +extern int git_net_url_fmt(git_buf *out, git_net_url *url); + +/** Place the path and query string into the given buffer. */ +extern int git_net_url_fmt_path(git_buf *buf, git_net_url *url); /** Disposes the contents of the structure. */ -void git_net_url_dispose(git_net_url *url); +extern void git_net_url_dispose(git_net_url *url); #endif diff --git a/src/netops.c b/src/netops.c index c885d5e89..04ae824cc 100644 --- a/src/netops.c +++ b/src/netops.c @@ -121,101 +121,3 @@ int gitno__match_host(const char *pattern, const char *host) return -1; } - -int gitno_connection_data_handle_redirect( - git_net_url *url, - const char *redirect_str, - const char *service_suffix) -{ - git_net_url tmp = GIT_NET_URL_INIT; - int error = 0; - - assert(url && redirect_str); - - if (redirect_str[0] == '/') { - git__free(url->path); - - if ((url->path = git__strdup(redirect_str)) == NULL) { - error = -1; - goto done; - } - } else { - git_net_url *original = url; - - if ((error = git_net_url_parse(&tmp, redirect_str)) < 0) - goto done; - - /* Validate that this is a legal redirection */ - - 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); - - error = -1; - goto done; - } - - 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); - - error = -1; - goto done; - } - - git_net_url_swap(url, &tmp); - } - - /* Remove the service suffix if it was given to us */ - if (service_suffix) { - /* - * Some servers strip the query parameters from the Location header - * when sending a redirect. Others leave it in place. - * Check for both, starting with the stripped case first, - * since it appears to be more common. - */ - const char *service_query = strchr(service_suffix, '?'); - size_t full_suffix_len = strlen(service_suffix); - size_t suffix_len = service_query ? - (size_t)(service_query - service_suffix) : full_suffix_len; - size_t path_len = strlen(url->path); - ssize_t truncate = -1; - - /* Check for a redirect without query parameters, like "/newloc/info/refs" */ - if (suffix_len && path_len >= suffix_len) { - size_t suffix_offset = path_len - suffix_len; - - if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 && - (!service_query || git__strcmp(url->query, service_query + 1) == 0)) { - truncate = suffix_offset; - } - } - - /* - * If we haven't already found where to truncate to remove the suffix, - * check for a redirect with query parameters, - * like "/newloc/info/refs?service=git-upload-pack" - */ - if (truncate == -1 && git__suffixcmp(url->path, service_suffix) == 0) { - truncate = path_len - full_suffix_len; - } - - if (truncate >= 0) { - /* Ensure we leave a minimum of '/' as the path */ - if (truncate == 0) - truncate++; - url->path[truncate] = '\0'; - - git__free(url->query); - url->query = NULL; - } - } - -done: - git_net_url_dispose(&tmp); - return error; -} - diff --git a/src/netops.h b/src/netops.h index 4c4bf78b0..52f1cccb6 100644 --- a/src/netops.h +++ b/src/netops.h @@ -65,15 +65,4 @@ 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); -/* - * 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_handle_redirect( - git_net_url *data, - const char *url, - const char *service_suffix); - #endif diff --git a/src/settings.c b/src/settings.c index 28d10eabb..6fae49eaf 100644 --- a/src/settings.c +++ b/src/settings.c @@ -25,6 +25,7 @@ #include "refs.h" #include "index.h" #include "transports/smart.h" +#include "transports/http.h" #include "streams/openssl.h" #include "streams/mbedtls.h" @@ -284,6 +285,10 @@ int git_libgit2_opts(int key, ...) git_disable_pack_keep_file_checks = (va_arg(ap, int) != 0); break; + case GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE: + git_http__expect_continue = (va_arg(ap, int) != 0); + break; + default: git_error_set(GIT_ERROR_INVALID, "invalid option key"); error = -1; diff --git a/src/trace.h b/src/trace.h index 6cf16776f..e15118ef5 100644 --- a/src/trace.h +++ b/src/trace.h @@ -56,7 +56,7 @@ GIT_INLINE(void) git_trace__null( GIT_UNUSED(fmt); } -#define git_trace_level() ((void)0) +#define git_trace_level() ((git_trace_level_t)0) #define git_trace git_trace__null #endif diff --git a/src/transports/auth_negotiate.c b/src/transports/auth_negotiate.c index 260fc1ceb..8fa44cd72 100644 --- a/src/transports/auth_negotiate.c +++ b/src/transports/auth_negotiate.c @@ -75,6 +75,22 @@ static int negotiate_set_challenge( return 0; } +static void negotiate_context_dispose(http_auth_negotiate_context *ctx) +{ + OM_uint32 status_minor; + + if (ctx->gss_context != GSS_C_NO_CONTEXT) { + gss_delete_sec_context( + &status_minor, &ctx->gss_context, GSS_C_NO_BUFFER); + ctx->gss_context = GSS_C_NO_CONTEXT; + } + + git_buf_dispose(&ctx->target); + + git__free(ctx->challenge); + ctx->challenge = NULL; +} + static int negotiate_next_token( git_buf *buf, git_http_auth_context *c, @@ -105,18 +121,20 @@ static int negotiate_next_token( if (GSS_ERROR(status_major)) { negotiate_err_set(status_major, status_minor, - "Could not parse principal"); + "could not parse principal"); error = -1; goto done; } challenge_len = ctx->challenge ? strlen(ctx->challenge) : 0; - if (challenge_len < 9) { - git_error_set(GIT_ERROR_NET, "no negotiate challenge sent from server"); + if (challenge_len < 9 || memcmp(ctx->challenge, "Negotiate", 9) != 0) { + git_error_set(GIT_ERROR_NET, "server did not request negotiate"); error = -1; goto done; - } else if (challenge_len > 9) { + } + + if (challenge_len > 9) { if (git_buf_decode_base64(&input_buf, ctx->challenge + 10, challenge_len - 10) < 0) { git_error_set(GIT_ERROR_NET, "invalid negotiate challenge from server"); @@ -128,14 +146,12 @@ static int negotiate_next_token( input_token.length = input_buf.size; input_token_ptr = &input_token; } else if (ctx->gss_context != GSS_C_NO_CONTEXT) { - git_error_set(GIT_ERROR_NET, "could not restart authentication"); - error = -1; - goto done; + negotiate_context_dispose(ctx); } mech = &negotiate_oid_spnego; - if (GSS_ERROR(status_major = gss_init_sec_context( + status_major = gss_init_sec_context( &status_minor, GSS_C_NO_CREDENTIAL, &ctx->gss_context, @@ -148,7 +164,9 @@ static int negotiate_next_token( NULL, &output_token, NULL, - NULL))) { + NULL); + + if (GSS_ERROR(status_major)) { negotiate_err_set(status_major, status_minor, "negotiate failure"); error = -1; goto done; @@ -156,10 +174,17 @@ static int negotiate_next_token( /* This message merely told us auth was complete; we do not respond. */ if (status_major == GSS_S_COMPLETE) { + negotiate_context_dispose(ctx); ctx->complete = 1; goto done; } + if (output_token.length == 0) { + git_error_set(GIT_ERROR_NET, "GSSAPI did not return token"); + error = -1; + goto done; + } + git_buf_puts(buf, "Negotiate "); git_buf_encode_base64(buf, output_token.value, output_token.length); @@ -185,17 +210,8 @@ static int negotiate_is_complete(git_http_auth_context *c) static void negotiate_context_free(git_http_auth_context *c) { http_auth_negotiate_context *ctx = (http_auth_negotiate_context *)c; - OM_uint32 status_minor; - if (ctx->gss_context != GSS_C_NO_CONTEXT) { - gss_delete_sec_context( - &status_minor, &ctx->gss_context, GSS_C_NO_BUFFER); - ctx->gss_context = GSS_C_NO_CONTEXT; - } - - git_buf_dispose(&ctx->target); - - git__free(ctx->challenge); + negotiate_context_dispose(ctx); ctx->configured = 0; ctx->complete = 0; @@ -214,8 +230,9 @@ static int negotiate_init_context( size_t i; /* Query supported mechanisms looking for SPNEGO) */ - if (GSS_ERROR(status_major = - gss_indicate_mechs(&status_minor, &mechanism_list))) { + status_major = gss_indicate_mechs(&status_minor, &mechanism_list); + + if (GSS_ERROR(status_major)) { negotiate_err_set(status_major, status_minor, "could not query mechanisms"); return -1; diff --git a/src/transports/auth_ntlm.h b/src/transports/auth_ntlm.h index 5b42b2b8e..a7cd6d795 100644 --- a/src/transports/auth_ntlm.h +++ b/src/transports/auth_ntlm.h @@ -11,6 +11,9 @@ #include "git2.h" #include "auth.h" +/* NTLM requires a full request/challenge/response */ +#define GIT_AUTH_STEPS_NTLM 2 + #ifdef GIT_NTLM #if defined(GIT_OPENSSL) diff --git a/src/transports/http.c b/src/transports/http.c index 045b72157..36f038ead 100644 --- a/src/transports/http.c +++ b/src/transports/http.c @@ -22,403 +22,82 @@ #include "http.h" #include "auth_negotiate.h" #include "auth_ntlm.h" +#include "trace.h" #include "streams/tls.h" #include "streams/socket.h" +#include "httpclient.h" -git_http_auth_scheme auth_schemes[] = { - { GIT_HTTP_AUTH_NEGOTIATE, "Negotiate", GIT_CREDTYPE_DEFAULT, git_http_auth_negotiate }, - { GIT_HTTP_AUTH_NTLM, "NTLM", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_ntlm }, - { GIT_HTTP_AUTH_BASIC, "Basic", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_basic }, -}; - -static const char *upload_pack_service = "upload-pack"; -static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; -static const char *upload_pack_service_url = "/git-upload-pack"; -static const char *receive_pack_service = "receive-pack"; -static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack"; -static const char *receive_pack_service_url = "/git-receive-pack"; -static const char *get_verb = "GET"; -static const char *post_verb = "POST"; - -#define AUTH_HEADER_SERVER "Authorization" -#define AUTH_HEADER_PROXY "Proxy-Authorization" - -#define SERVER_TYPE_REMOTE "remote" -#define SERVER_TYPE_PROXY "proxy" - -#define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport) +bool git_http__expect_continue = false; -#define PARSE_ERROR_GENERIC -1 -#define PARSE_ERROR_REPLAY -2 -/** Look at the user field */ -#define PARSE_ERROR_EXT -3 +typedef enum { + HTTP_STATE_NONE = 0, + HTTP_STATE_SENDING_REQUEST, + HTTP_STATE_RECEIVING_RESPONSE, + HTTP_STATE_DONE +} http_state; -#define CHUNK_SIZE 4096 - -enum last_cb { - NONE, - FIELD, - VALUE -}; +typedef struct { + git_http_method method; + const char *url; + const char *request_type; + const char *response_type; + unsigned chunked : 1; +} http_service; typedef struct { git_smart_subtransport_stream parent; - const char *service; - const char *service_url; - char *redirect_url; - const char *verb; - char *chunk_buffer; - unsigned chunk_buffer_len; - unsigned sent_request : 1, - received_response : 1, - chunked : 1; + const http_service *service; + http_state state; + unsigned replay_count; } http_stream; typedef struct { git_net_url url; - git_stream *stream; - - git_http_auth_t authtypes; - git_credtype_t credtypes; git_cred *cred; - unsigned url_cred_presented : 1, - authenticated : 1; - - git_vector auth_challenges; - git_http_auth_context *auth_context; + unsigned auth_schemetypes; + unsigned url_cred_presented : 1; } http_server; typedef struct { git_smart_subtransport parent; transport_smart *owner; - git_stream *gitserver_stream; - bool connected; http_server server; - http_server proxy; - char *proxy_url; - git_proxy_options proxy_opts; - - /* Parser structures */ - http_parser parser; - http_parser_settings settings; - gitno_buffer parse_buffer; - git_buf parse_header_name; - git_buf parse_header_value; - char parse_buffer_data[NETIO_BUFSIZE]; - char *content_type; - char *content_length; - char *location; - enum last_cb last_cb; - int parse_error; - int error; - unsigned request_count; - unsigned parse_finished : 1, - keepalive : 1, - replay_count : 4; -} http_subtransport; - -typedef struct { - http_stream *s; - http_subtransport *t; - - /* Target buffer details from read() */ - char *buffer; - size_t buf_size; - size_t *bytes_read; -} parser_context; - -static git_http_auth_scheme *scheme_for_challenge( - const char *challenge, - git_cred *cred) -{ - git_http_auth_scheme *scheme = NULL; - size_t i; - - for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { - const char *scheme_name = auth_schemes[i].name; - const git_credtype_t scheme_types = auth_schemes[i].credtypes; - size_t scheme_len; - - scheme_len = strlen(scheme_name); - - if ((!cred || (cred->credtype & scheme_types)) && - strncasecmp(challenge, scheme_name, scheme_len) == 0 && - (challenge[scheme_len] == '\0' || challenge[scheme_len] == ' ')) { - scheme = &auth_schemes[i]; - break; - } - } - - return scheme; -} - -static int apply_credentials( - git_buf *buf, - http_server *server, - const char *header_name) -{ - git_buf token = GIT_BUF_INIT; - int error = 0; - - if (!server->auth_context) - goto done; - - if ((error = server->auth_context->next_token(&token, server->auth_context, server->cred)) < 0) - goto done; - - error = git_buf_printf(buf, "%s: %s\r\n", header_name, token.ptr); - -done: - git_buf_dispose(&token); - return error; -} - -static int gen_request( - git_buf *buf, - http_stream *s, - size_t content_length) -{ - http_subtransport *t = OWNING_SUBTRANSPORT(s); - const char *path = t->server.url.path ? t->server.url.path : "/"; - const char *service_url = s->service_url; - size_t i; - /* If path already ends in /, remove the leading slash from service_url */ - if ((git__suffixcmp(path, "/") == 0) && (git__prefixcmp(service_url, "/") == 0)) - service_url++; - - 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.scheme, - t->server.url.host, - t->server.url.port, - path, service_url); - else - git_buf_printf(buf, "%s %s%s HTTP/1.1\r\n", - s->verb, path, service_url); - - git_buf_puts(buf, "User-Agent: "); - git_http__user_agent(buf); - git_buf_puts(buf, "\r\n"); - git_buf_printf(buf, "Host: %s", t->server.url.host); - - 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) { - git_buf_printf(buf, "Accept: application/x-git-%s-result\r\n", s->service); - git_buf_printf(buf, "Content-Type: application/x-git-%s-request\r\n", s->service); - - if (s->chunked) - git_buf_puts(buf, "Transfer-Encoding: chunked\r\n"); - else - git_buf_printf(buf, "Content-Length: %"PRIuZ "\r\n", content_length); - } else - git_buf_puts(buf, "Accept: */*\r\n"); - - for (i = 0; i < t->owner->custom_headers.count; i++) { - if (t->owner->custom_headers.strings[i]) - git_buf_printf(buf, "%s\r\n", t->owner->custom_headers.strings[i]); - } - - /* Apply proxy and server credentials to the request */ - if (t->proxy_opts.type != GIT_PROXY_NONE && - apply_credentials(buf, &t->proxy, AUTH_HEADER_PROXY) < 0) - return -1; - - if (apply_credentials(buf, &t->server, AUTH_HEADER_SERVER) < 0) - return -1; - - git_buf_puts(buf, "\r\n"); - if (git_buf_oom(buf)) - return -1; - - return 0; -} - -static int set_authentication_challenge(http_server *server) -{ - 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 ((scheme = scheme_for_challenge(challenge, NULL)) != NULL) { - server->authtypes |= scheme->type; - server->credtypes |= scheme->credtypes; - } - } - - 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); - - server->authenticated = 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) -{ - git_buf *name = &t->parse_header_name; - git_buf *value = &t->parse_header_value; - - if (!strcasecmp("Content-Type", git_buf_cstr(name))) { - if (t->content_type) { - git_error_set(GIT_ERROR_NET, "multiple Content-Type headers"); - return -1; - } - - t->content_type = git__strdup(git_buf_cstr(value)); - GIT_ERROR_CHECK_ALLOC(t->content_type); - } - else if (!strcasecmp("Content-Length", git_buf_cstr(name))) { - if (t->content_length) { - git_error_set(GIT_ERROR_NET, "multiple Content-Length headers"); - return -1; - } - - t->content_length = git__strdup(git_buf_cstr(value)); - GIT_ERROR_CHECK_ALLOC(t->content_length); - } - else if (!strcasecmp("Proxy-Authenticate", git_buf_cstr(name))) { - char *dup = git__strdup(git_buf_cstr(value)); - GIT_ERROR_CHECK_ALLOC(dup); - - if (git_vector_insert(&t->proxy.auth_challenges, dup) < 0) - return -1; - } - else if (!strcasecmp("WWW-Authenticate", git_buf_cstr(name))) { - char *dup = git__strdup(git_buf_cstr(value)); - GIT_ERROR_CHECK_ALLOC(dup); - - if (git_vector_insert(&t->server.auth_challenges, dup) < 0) - return -1; - } - else if (!strcasecmp("Location", git_buf_cstr(name))) { - if (t->location) { - git_error_set(GIT_ERROR_NET, "multiple Location headers"); - return -1; - } - - t->location = git__strdup(git_buf_cstr(value)); - GIT_ERROR_CHECK_ALLOC(t->location); - } - - return 0; -} - -static int on_header_field(http_parser *parser, const char *str, size_t len) -{ - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - - /* 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; - - if (NONE == t->last_cb || VALUE == t->last_cb) - git_buf_clear(&t->parse_header_name); - - if (git_buf_put(&t->parse_header_name, str, len) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; - - t->last_cb = FIELD; - return 0; -} - -static int on_header_value(http_parser *parser, const char *str, size_t len) -{ - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - - assert(NONE != t->last_cb); - - if (FIELD == t->last_cb) - git_buf_clear(&t->parse_header_value); + git_http_client *http_client; +} http_subtransport; - if (git_buf_put(&t->parse_header_value, str, len) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; +static const http_service upload_pack_ls_service = { + GIT_HTTP_METHOD_GET, "/info/refs?service=git-upload-pack", + NULL, + "application/x-git-upload-pack-advertisement", + 0 +}; +static const http_service upload_pack_service = { + GIT_HTTP_METHOD_POST, "/git-upload-pack", + "application/x-git-upload-pack-request", + "application/x-git-upload-pack-result", + 0 +}; +static const http_service receive_pack_ls_service = { + GIT_HTTP_METHOD_GET, "/info/refs?service=git-receive-pack", + NULL, + "application/x-git-receive-pack-advertisement", + 0 +}; +static const http_service receive_pack_service = { + GIT_HTTP_METHOD_POST, "/git-receive-pack", + "application/x-git-receive-pack-request", + "application/x-git-receive-pack-result", + 1 +}; - t->last_cb = VALUE; - return 0; -} +#define SERVER_TYPE_REMOTE "remote" +#define SERVER_TYPE_PROXY "proxy" -GIT_INLINE(void) free_cred(git_cred **cred) -{ - if (*cred) { - git_cred_free(*cred); - (*cred) = NULL; - } -} +#define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport) static int apply_url_credentials( git_cred **cred, @@ -435,1105 +114,532 @@ static int apply_url_credentials( return GIT_PASSTHROUGH; } -static int init_auth(http_server *server) +GIT_INLINE(void) free_cred(git_cred **cred) { - git_http_auth_scheme *s, *scheme = NULL; - char *c, *challenge = NULL; - size_t i; - int error; - - git_vector_foreach(&server->auth_challenges, i, c) { - s = scheme_for_challenge(c, server->cred); - - 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 (*cred) { + git_cred_free(*cred); + (*cred) = NULL; } - - if ((error = scheme->init_context(&server->auth_context, &server->url)) == GIT_PASSTHROUGH) - return 0; - else if (error < 0) - return error; - - if (server->auth_context->set_challenge && - (error = server->auth_context->set_challenge(server->auth_context, challenge)) < 0) - return error; - - return 0; } -static int on_auth_required( - http_parser *parser, +static int handle_auth( http_server *server, + const char *server_type, const char *url, - const char *type, + unsigned int allowed_schemetypes, + unsigned int allowed_credtypes, git_cred_acquire_cb callback, void *callback_payload) { - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; int error = 1; - if (parse_authenticate_response(server) < 0) { - t->parse_error = PARSE_ERROR_GENERIC; - return t->parse_error; - } - - /* 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; - } - - free_auth_context(server); - free_cred(&server->cred); + if (server->cred) + 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); + if ((allowed_credtypes & GIT_CREDTYPE_USERPASS_PLAINTEXT) && + !server->url_cred_presented && + server->url.username && + server->url.password) { + error = apply_url_credentials(&server->cred, allowed_credtypes, server->url.username, server->url.password); server->url_cred_presented = 1; - if (error == GIT_PASSTHROUGH) { - /* treat GIT_PASSTHROUGH as if callback isn't set */ + /* treat GIT_PASSTHROUGH as if callback isn't set */ + if (error == GIT_PASSTHROUGH) error = 1; - } } if (error > 0 && callback) { - error = callback(&server->cred, url, server->url.username, server->credtypes, callback_payload); + error = callback(&server->cred, url, server->url.username, allowed_credtypes, callback_payload); - if (error == GIT_PASSTHROUGH) { - /* treat GIT_PASSTHROUGH as if callback isn't set */ + /* treat GIT_PASSTHROUGH as if callback isn't set */ + if (error == GIT_PASSTHROUGH) error = 1; - } } 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; + git_error_set(GIT_ERROR_HTTP, "%s authentication required but no callback set", server_type); + error = -1; } - /* 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; -} + if (!error) + server->auth_schemetypes = allowed_schemetypes; -static void on_auth_success(http_server *server) -{ - server->url_cred_presented = 0; - server->authenticated = 1; + return error; } -static int on_headers_complete(http_parser *parser) +GIT_INLINE(int) handle_remote_auth( + http_stream *stream, + git_http_response *response) { - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - http_stream *s = ctx->s; - git_buf buf = GIT_BUF_INIT; - - /* Both parse_header_name and parse_header_value are populated - * and ready for consumption. */ - 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( - parser, - &t->proxy, - t->proxy_opts.url, - SERVER_TYPE_PROXY, - t->proxy_opts.credentials, - 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( - parser, - &t->server, - t->owner->url, - SERVER_TYPE_REMOTE, - t->owner->cred_acquire_cb, - 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. */ - if ((parser->status_code == 301 || - parser->status_code == 302 || - (parser->status_code == 303 && get_verb == s->verb) || - parser->status_code == 307 || - parser->status_code == 308) && - t->location) { - - 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 - * ownership of the memory. */ - if (s->redirect_url) - git__free(s->redirect_url); - - s->redirect_url = t->location; - t->location = NULL; - - t->connected = 0; - t->parse_error = PARSE_ERROR_REPLAY; - return 0; - } + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); - /* Check for a 200 HTTP status code. */ - if (parser->status_code != 200) { - git_error_set(GIT_ERROR_NET, - "unexpected HTTP status code: %d", - parser->status_code); - return t->parse_error = PARSE_ERROR_GENERIC; - } - - /* The response must contain a Content-Type header. */ - if (!t->content_type) { - git_error_set(GIT_ERROR_NET, "no Content-Type header in response"); - return t->parse_error = PARSE_ERROR_GENERIC; - } - - /* The Content-Type header must match our expectation. */ - if (get_verb == s->verb) - git_buf_printf(&buf, - "application/x-git-%s-advertisement", - ctx->s->service); - else - git_buf_printf(&buf, - "application/x-git-%s-result", - ctx->s->service); - - if (git_buf_oom(&buf)) - return t->parse_error = PARSE_ERROR_GENERIC; - - if (strcmp(t->content_type, git_buf_cstr(&buf))) { - git_buf_dispose(&buf); - git_error_set(GIT_ERROR_NET, - "invalid Content-Type: %s", - t->content_type); - return t->parse_error = PARSE_ERROR_GENERIC; + if (response->server_auth_credtypes == 0) { + git_error_set(GIT_ERROR_HTTP, "server requires authentication that we do not support"); + return -1; } - git_buf_dispose(&buf); - - return 0; -} - -static int on_message_complete(http_parser *parser) -{ - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - - t->parse_finished = 1; - t->keepalive = http_should_keep_alive(parser); - - return 0; + /* Otherwise, prompt for credentials. */ + return handle_auth( + &transport->server, + SERVER_TYPE_REMOTE, + transport->owner->url, + response->server_auth_schemetypes, + response->server_auth_credtypes, + transport->owner->cred_acquire_cb, + transport->owner->cred_acquire_payload); } -static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len) +GIT_INLINE(int) handle_proxy_auth( + http_stream *stream, + git_http_response *response) { - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - - /* If there's no buffer set, we're explicitly ignoring the body. */ - if (ctx->buffer) { - if (ctx->buf_size < len) { - git_error_set(GIT_ERROR_NET, "can't fit data in the buffer"); - return t->parse_error = PARSE_ERROR_GENERIC; - } + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); - memcpy(ctx->buffer, str, len); - ctx->buffer += len; - ctx->buf_size -= len; + if (response->proxy_auth_credtypes == 0) { + git_error_set(GIT_ERROR_HTTP, "proxy requires authentication that we do not support"); + return -1; } - *(ctx->bytes_read) += len; - - return 0; + /* Otherwise, prompt for credentials. */ + return handle_auth( + &transport->proxy, + SERVER_TYPE_PROXY, + transport->owner->proxy.url, + response->server_auth_schemetypes, + response->proxy_auth_credtypes, + transport->owner->proxy.credentials, + transport->owner->proxy.payload); } -static void clear_parser_state(http_subtransport *t) -{ - http_parser_init(&t->parser, HTTP_RESPONSE); - gitno_buffer_setup_fromstream(t->server.stream, - &t->parse_buffer, - t->parse_buffer_data, - sizeof(t->parse_buffer_data)); - - 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); - git_buf_dispose(&t->parse_header_value); - git_buf_init(&t->parse_header_value, 0); +static int handle_response( + bool *complete, + http_stream *stream, + git_http_response *response, + bool allow_replay) +{ + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + int error; - git__free(t->content_type); - t->content_type = NULL; + *complete = false; - git__free(t->content_length); - t->content_length = NULL; + if (allow_replay && git_http_response_is_redirect(response)) { + if (!response->location) { + git_error_set(GIT_ERROR_HTTP, "redirect without location"); + return -1; + } - git__free(t->location); - t->location = NULL; + if (git_net_url_apply_redirect(&transport->server.url, response->location, stream->service->url) < 0) { + return -1; + } - git_vector_free_deep(&t->proxy.auth_challenges); - git_vector_free_deep(&t->server.auth_challenges); -} + return 0; + } else if (git_http_response_is_redirect(response)) { + git_error_set(GIT_ERROR_HTTP, "unexpected redirect"); + return -1; + } -static int write_chunk(git_stream *io, const char *buffer, size_t len) -{ - git_buf buf = GIT_BUF_INIT; + /* If we're in the middle of challenge/response auth, continue. */ + if (allow_replay && response->resend_credentials) { + return 0; + } else if (allow_replay && response->status == GIT_HTTP_STATUS_UNAUTHORIZED) { + if ((error = handle_remote_auth(stream, response)) < 0) + return error; - /* Chunk header */ - git_buf_printf(&buf, "%" PRIxZ "\r\n", len); + return git_http_client_skip_body(transport->http_client); + } else if (allow_replay && response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { + if ((error = handle_proxy_auth(stream, response)) < 0) + return error; - if (git_buf_oom(&buf)) + return git_http_client_skip_body(transport->http_client); + } else if (response->status == GIT_HTTP_STATUS_UNAUTHORIZED || + response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { + git_error_set(GIT_ERROR_HTTP, "unexpected authentication failure"); return -1; + } - if (git_stream__write_full(io, buf.ptr, buf.size, 0) < 0) { - git_buf_dispose(&buf); + if (response->status != GIT_HTTP_STATUS_OK) { + git_error_set(GIT_ERROR_HTTP, "unexpected http status code: %d", response->status); return -1; } - git_buf_dispose(&buf); - - /* Chunk body */ - if (len > 0 && git_stream__write_full(io, buffer, len, 0) < 0) + /* The response must contain a Content-Type header. */ + if (!response->content_type) { + git_error_set(GIT_ERROR_HTTP, "no content-type header in response"); return -1; + } - /* Chunk footer */ - if (git_stream__write_full(io, "\r\n", 2, 0) < 0) + /* The Content-Type header must match our expectation. */ + if (strcmp(response->content_type, stream->service->response_type) != 0) { + git_error_set(GIT_ERROR_HTTP, "invalid content-type: '%s'", response->content_type); return -1; + } + *complete = true; + stream->state = HTTP_STATE_RECEIVING_RESPONSE; return 0; } -static int load_proxy_config(http_subtransport *t) +static int lookup_proxy( + bool *out_use, + http_subtransport *transport) { - int error; - - switch (t->owner->proxy.type) { - case GIT_PROXY_NONE: - return 0; + const char *proxy; + git_remote *remote; + bool use_ssl; + char *config = NULL; + int error = 0; - case GIT_PROXY_AUTO: - git__free(t->proxy_url); - t->proxy_url = NULL; + *out_use = false; + git_net_url_dispose(&transport->proxy.url); - git_proxy_options_init(&t->proxy_opts, GIT_PROXY_OPTIONS_VERSION); + switch (transport->owner->proxy.type) { + case GIT_PROXY_SPECIFIED: + proxy = transport->owner->proxy.url; + break; - if ((error = git_remote__get_http_proxy(t->owner->owner, - !strcmp(t->server.url.scheme, "https"), &t->proxy_url)) < 0) - return error; + case GIT_PROXY_AUTO: + remote = transport->owner->owner; + use_ssl = !strcmp(transport->server.url.scheme, "https"); - if (!t->proxy_url) - return 0; + error = git_remote__get_http_proxy(remote, use_ssl, &config); - t->proxy_opts.type = GIT_PROXY_SPECIFIED; - t->proxy_opts.url = t->proxy_url; - t->proxy_opts.credentials = t->owner->proxy.credentials; - t->proxy_opts.certificate_check = t->owner->proxy.certificate_check; - t->proxy_opts.payload = t->owner->proxy.payload; - break; + if (error || !config) + goto done; - case GIT_PROXY_SPECIFIED: - memcpy(&t->proxy_opts, &t->owner->proxy, sizeof(git_proxy_options)); + proxy = config; break; default: - assert(0); - return -1; + return 0; } - git_net_url_dispose(&t->proxy.url); - - return git_net_url_parse(&t->proxy.url, t->proxy_opts.url); -} - -static int check_certificate( - git_stream *stream, - git_net_url *url, - int is_valid, - git_transport_certificate_check_cb cert_cb, - void *cert_cb_payload) -{ - git_cert *cert; - git_error_state last_error = {0}; - int error; - - if ((error = git_stream_certificate(&cert, stream)) < 0) - return error; - - git_error_state_capture(&last_error, GIT_ECERTIFICATE); - - error = cert_cb(cert, is_valid, url->host, cert_cb_payload); + if (!proxy || + (error = git_net_url_parse(&transport->proxy.url, proxy)) < 0) + goto done; - if (error == GIT_PASSTHROUGH && !is_valid) - return git_error_state_restore(&last_error); - else if (error == GIT_PASSTHROUGH) - error = 0; - else if (error && !git_error_last()) - git_error_set(GIT_ERROR_NET, "user rejected certificate for %s", url->host); + *out_use = true; - git_error_state_free(&last_error); +done: + git__free(config); return error; } -static int stream_connect( - git_stream *stream, +static int generate_request( git_net_url *url, - git_transport_certificate_check_cb cert_cb, - void *cb_payload) + git_http_request *request, + http_stream *stream, + size_t len) { + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + bool use_proxy = false; int error; - GIT_ERROR_CHECK_VERSION(stream, GIT_STREAM_VERSION, "git_stream"); - - error = git_stream_connect(stream); - - if (error && error != GIT_ECERTIFICATE) + if ((error = git_net_url_joinpath(url, + &transport->server.url, stream->service->url)) < 0 || + (error = lookup_proxy(&use_proxy, transport)) < 0) return error; - if (git_stream_is_encrypted(stream) && cert_cb != NULL) - error = check_certificate(stream, url, !error, cert_cb, cb_payload); - - return error; -} - -static int gen_connect_req(git_buf *buf, http_subtransport *t) -{ - git_buf_printf(buf, "CONNECT %s:%s HTTP/1.1\r\n", - t->server.url.host, t->server.url.port); - - git_buf_puts(buf, "User-Agent: "); - git_http__user_agent(buf); - git_buf_puts(buf, "\r\n"); - - git_buf_printf(buf, "Host: %s\r\n", t->proxy.url.host); - - if (apply_credentials(buf, &t->proxy, AUTH_HEADER_PROXY) < 0) - return -1; - - git_buf_puts(buf, "\r\n"); - - return git_buf_oom(buf) ? -1 : 0; -} - -static int proxy_headers_complete(http_parser *parser) -{ - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - - /* Both parse_header_name and parse_header_value are populated - * and ready for consumption. */ - if (t->last_cb == VALUE && on_header_ready(t) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; + request->method = stream->service->method; + request->url = url; + request->credentials = transport->server.cred; + request->proxy = use_proxy ? &transport->proxy.url : NULL; + request->proxy_credentials = transport->proxy.cred; - /* - * 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) < 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( - parser, - &t->proxy, - t->proxy_opts.url, - SERVER_TYPE_PROXY, - t->proxy_opts.credentials, - t->proxy_opts.payload); - - if (parser->status_code != 200) { - git_error_set(GIT_ERROR_NET, "unexpected status code from proxy: %d", - parser->status_code); - return t->parse_error = PARSE_ERROR_GENERIC; + if (stream->service->method == GIT_HTTP_METHOD_POST) { + request->chunked = stream->service->chunked; + request->content_length = stream->service->chunked ? 0 : len; + request->content_type = stream->service->request_type; + request->accept = stream->service->response_type; + request->expect_continue = git_http__expect_continue; } - if (!t->content_length || strcmp(t->content_length, "0") == 0) - t->parse_finished = 1; - return 0; } -static int proxy_connect( - git_stream **out, git_stream *proxy_stream, http_subtransport *t) -{ - git_buf request = GIT_BUF_INIT; - static http_parser_settings proxy_parser_settings = {0}; - size_t bytes_read = 0, bytes_parsed; - parser_context ctx; - bool auth_replay; +/* + * Read from an HTTP transport - for the first invocation of this function + * (ie, when stream->state == HTTP_STATE_NONE), we'll send a GET request + * to the remote host. We will stream that data back on all subsequent + * calls. + */ +static int http_stream_read( + git_smart_subtransport_stream *s, + char *buffer, + size_t buffer_size, + size_t *out_len) +{ + http_stream *stream = (http_stream *)s; + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_net_url url = GIT_NET_URL_INIT; + git_net_url proxy_url = GIT_NET_URL_INIT; + git_http_request request = {0}; + git_http_response response = {0}; + bool complete; int error; - /* Use the parser settings only to parser headers. */ - proxy_parser_settings.on_header_field = on_header_field; - proxy_parser_settings.on_header_value = on_header_value; - proxy_parser_settings.on_headers_complete = proxy_headers_complete; - proxy_parser_settings.on_message_complete = on_message_complete; - -replay: - clear_parser_state(t); - - auth_replay = false; - - gitno_buffer_setup_fromstream(proxy_stream, - &t->parse_buffer, - t->parse_buffer_data, - sizeof(t->parse_buffer_data)); + *out_len = 0; - if ((error = gen_connect_req(&request, t)) < 0) - goto done; - - if ((error = git_stream__write_full(proxy_stream, request.ptr, - request.size, 0)) < 0) - goto done; - - git_buf_dispose(&request); - - while (!bytes_read && !t->parse_finished) { - t->parse_buffer.offset = 0; + if (stream->state == HTTP_STATE_NONE) { + stream->state = HTTP_STATE_SENDING_REQUEST; + stream->replay_count = 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; + /* + * Formulate the URL, send the request and read the response + * headers. Some of the request body may also be read. + */ + while (stream->state == HTTP_STATE_SENDING_REQUEST && + stream->replay_count < GIT_HTTP_REPLAY_MAX) { + git_net_url_dispose(&url); + git_net_url_dispose(&proxy_url); + git_http_response_dispose(&response); + + if ((error = generate_request(&url, &request, stream, 0)) < 0 || + (error = git_http_client_send_request( + transport->http_client, &request)) < 0 || + (error = git_http_client_read_response( + &response, transport->http_client)) < 0 || + (error = handle_response(&complete, stream, &response, true)) < 0) goto done; - } - - /* - * This call to http_parser_execute will invoke the on_* - * callbacks. Since we don't care about the body of the response, - * we can set our buffer to NULL. - */ - ctx.t = t; - ctx.s = NULL; - ctx.buffer = NULL; - ctx.buf_size = 0; - ctx.bytes_read = &bytes_read; - - /* Set the context, call the parser, then unset the context. */ - t->parser.data = &ctx; - - bytes_parsed = http_parser_execute(&t->parser, - &proxy_parser_settings, t->parse_buffer.data, t->parse_buffer.offset); - t->parser.data = NULL; - - /* Ensure that we didn't get a redirect; unsupported. */ - if (t->location) { - git_error_set(GIT_ERROR_NET, "proxy server sent unsupported redirect during CONNECT"); - error = -1; - goto done; - } + if (complete) + break; - /* Replay the request with authentication headers. */ - 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; - } + stream->replay_count++; + } - if (bytes_parsed != t->parse_buffer.offset) { - git_error_set(GIT_ERROR_NET, - "HTTP parser error: %s", - http_errno_description((enum http_errno)t->parser.http_errno)); - error = -1; - goto done; - } + if (stream->state == HTTP_STATE_SENDING_REQUEST) { + git_error_set(GIT_ERROR_HTTP, "too many redirects or authentication replays"); + error = -1; + goto done; } - t->request_count++; + assert (stream->state == HTTP_STATE_RECEIVING_RESPONSE); - if (auth_replay) { - if (t->keepalive && t->parse_finished) - goto replay; + error = git_http_client_read_body(transport->http_client, buffer, buffer_size); - return PARSE_ERROR_REPLAY; + if (error > 0) { + *out_len = error; + error = 0; } - 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, - t->owner->message_cb_payload); - - /* - * Since we've connected via a HTTPS proxy tunnel, we don't behave - * as if we have an HTTP proxy. - */ - t->proxy_opts.type = GIT_PROXY_NONE; - t->replay_count = 0; - t->request_count = 0; - done: + git_net_url_dispose(&url); + git_net_url_dispose(&proxy_url); + git_http_response_dispose(&response); + return error; } -static void reset_auth_connection(http_server *server) +static bool needs_probe(http_stream *stream) { - /* - * 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); + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); - server->url_cred_presented = 0; - server->authenticated = 0; - } + return (transport->server.auth_schemetypes == GIT_HTTP_AUTH_NTLM || + transport->server.auth_schemetypes == GIT_HTTP_AUTH_NEGOTIATE); } -static int http_connect(http_subtransport *t) +static int send_probe(http_stream *stream) { - git_net_url *url; - git_stream *proxy_stream = NULL, *stream = NULL; - git_transport_certificate_check_cb cert_cb; - void *cb_payload; + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_http_client *client = transport->http_client; + const char *probe = "0000"; + size_t len = 4; + git_net_url url = GIT_NET_URL_INIT; + git_http_request request = {0}; + git_http_response response = {0}; + bool complete = false; + size_t step, steps = 1; int error; -auth_replay: - if (t->connected && t->keepalive && t->parse_finished) - return 0; - - if ((error = load_proxy_config(t)) < 0) - return error; - - if (t->server.stream) { - git_stream_close(t->server.stream); - git_stream_free(t->server.stream); - t->server.stream = NULL; - } - - if (t->proxy.stream) { - git_stream_close(t->proxy.stream); - git_stream_free(t->proxy.stream); - 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; - cert_cb = t->proxy_opts.certificate_check; - cb_payload = t->proxy_opts.payload; - } else { - url = &t->server.url; - cert_cb = t->owner->certificate_check_cb; - cb_payload = t->owner->message_cb_payload; - } - - 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); - - if (error < 0) - goto on_error; - - if ((error = stream_connect(stream, url, cert_cb, cb_payload)) < 0) - goto on_error; + /* NTLM requires a full challenge/response */ + if (transport->server.auth_schemetypes == GIT_HTTP_AUTH_NTLM) + steps = GIT_AUTH_STEPS_NTLM; /* - * At this point we have a connection to the remote server or to - * a proxy. If it's a proxy and the remote server is actually - * an HTTPS connection, then we need to build a CONNECT tunnel. + * Send at most two requests: one without any authentication to see + * if we get prompted to authenticate. If we do, send a second one + * with the first authentication message. The final authentication + * message with the response will occur with the *actual* POST data. */ - if (t->proxy_opts.type == GIT_PROXY_SPECIFIED && - strcmp(t->server.url.scheme, "https") == 0) { - proxy_stream = stream; - stream = NULL; - - 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; - return 0; - -on_error: - if (stream) { - git_stream_close(stream); - git_stream_free(stream); - } - - if (proxy_stream) { - git_stream_close(proxy_stream); - git_stream_free(proxy_stream); + for (step = 0; step < steps && !complete; step++) { + git_net_url_dispose(&url); + git_http_response_dispose(&response); + + if ((error = generate_request(&url, &request, stream, len)) < 0 || + (error = git_http_client_send_request(client, &request)) < 0 || + (error = git_http_client_send_body(client, probe, len)) < 0 || + (error = git_http_client_read_response(&response, client)) < 0 || + (error = git_http_client_skip_body(client)) < 0 || + (error = handle_response(&complete, stream, &response, true)) < 0) + goto done; } +done: + git_http_response_dispose(&response); + git_net_url_dispose(&url); return error; } -static int http_stream_read( - git_smart_subtransport_stream *stream, - char *buffer, - size_t buf_size, - size_t *bytes_read) +/* +* Write to an HTTP transport - for the first invocation of this function +* (ie, when stream->state == HTTP_STATE_NONE), we'll send a POST request +* to the remote host. If we're sending chunked data, then subsequent calls +* will write the additional data given in the buffer. If we're not chunking, +* then the caller should have given us all the data in the original call. +* The caller should call http_stream_read_response to get the result. +*/ +static int http_stream_write( + git_smart_subtransport_stream *s, + const char *buffer, + size_t len) { - http_stream *s = (http_stream *)stream; - 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_clear(&request); - clear_parser_state(t); - - 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; - } - - if (!s->received_response) { - if (s->chunked) { - assert(s->verb == post_verb); - - /* Flush, if necessary */ - 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; - } + http_stream *stream = GIT_CONTAINER_OF(s, http_stream, parent); + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_net_url url = GIT_NET_URL_INIT; + git_http_request request = {0}; + git_http_response response = {0}; + int error; - /* Write the final chunk. */ - if ((error = git_stream__write_full(t->server.stream, - "0\r\n\r\n", 5, 0)) < 0) - goto done; - } + while (stream->state == HTTP_STATE_NONE && + stream->replay_count < GIT_HTTP_REPLAY_MAX) { - s->received_response = 1; - } - - while (!*bytes_read && !t->parse_finished) { - size_t data_offset; + git_net_url_dispose(&url); + git_http_response_dispose(&response); /* - * Make the parse_buffer think it's as full of data as - * the buffer, so it won't try to recv more data than - * we can put into it. - * - * data_offset is the actual data offset from which we - * should tell the parser to start reading. + * If we're authenticating with a connection-based mechanism + * (NTLM, Kerberos), send a "probe" packet. Servers SHOULD + * authenticate an entire keep-alive connection, so ideally + * we should not need to authenticate but some servers do + * not support this. By sending a probe packet, we'll be + * able to follow up with a second POST using the actual + * data (and, in the degenerate case, the authentication + * header as well). */ - if (buf_size >= t->parse_buffer.len) - t->parse_buffer.offset = 0; - else - t->parse_buffer.offset = t->parse_buffer.len - buf_size; - - data_offset = t->parse_buffer.offset; - - 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; + if (needs_probe(stream) && (error = send_probe(stream)) < 0) 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, 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 = 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. */ - t->parser.data = &ctx; - - bytes_parsed = http_parser_execute(&t->parser, - &t->settings, - t->parse_buffer.data + data_offset, - t->parse_buffer.offset - data_offset); - - t->parser.data = NULL; - - /* 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; + /* Send the regular POST request. */ + if ((error = generate_request(&url, &request, stream, len)) < 0 || + (error = git_http_client_send_request( + transport->http_client, &request)) < 0) goto done; - } else if (t->parse_error < 0) { - error = -1; - goto done; - } - 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)); - error = -1; - goto done; + if (request.expect_continue && + git_http_client_has_response(transport->http_client)) { + bool complete; + + /* + * If we got a response to an expect/continue, then + * it's something other than a 100 and we should + * deal with the response somehow. + */ + if ((error = git_http_client_read_response(&response, transport->http_client)) < 0 || + (error = handle_response(&complete, stream, &response, true)) < 0) + goto done; + } else { + stream->state = HTTP_STATE_SENDING_REQUEST; } - } - t->request_count++; + stream->replay_count++; + } - if (auth_replay) { - s->sent_request = 0; + if (stream->state == HTTP_STATE_NONE) { + git_error_set(GIT_ERROR_HTTP, + "too many redirects or authentication replays"); + error = -1; + goto done; + } - if ((error = http_connect(t)) < 0) - return error; + assert(stream->state == HTTP_STATE_SENDING_REQUEST); - goto replay; - } + error = git_http_client_send_body(transport->http_client, buffer, len); done: - git_buf_dispose(&request); + git_http_response_dispose(&response); + git_net_url_dispose(&url); return error; } -static int http_stream_write_chunked( - git_smart_subtransport_stream *stream, - const char *buffer, - size_t len) -{ - http_stream *s = GIT_CONTAINER_OF(stream, http_stream, parent); - http_subtransport *t = OWNING_SUBTRANSPORT(s); - - assert(t->connected); - - /* Send the request, if necessary */ - if (!s->sent_request) { - git_buf request = GIT_BUF_INIT; - - 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); - - s->sent_request = 1; - } - - if (len > CHUNK_SIZE) { - /* Flush, if necessary */ - if (s->chunk_buffer_len > 0) { - if (write_chunk(t->server.stream, - s->chunk_buffer, s->chunk_buffer_len) < 0) - return -1; - - s->chunk_buffer_len = 0; - } - - /* Write chunk directly */ - if (write_chunk(t->server.stream, buffer, len) < 0) - return -1; - } - else { - /* Append as much to the buffer as we can */ - int count = min(CHUNK_SIZE - s->chunk_buffer_len, len); - - if (!s->chunk_buffer) { - s->chunk_buffer = git__malloc(CHUNK_SIZE); - GIT_ERROR_CHECK_ALLOC(s->chunk_buffer); - } - - memcpy(s->chunk_buffer + s->chunk_buffer_len, buffer, count); - s->chunk_buffer_len += count; - buffer += count; - len -= count; +/* +* Read from an HTTP transport after it has been written to. This is the +* response from a POST request made by http_stream_write. +*/ +static int http_stream_read_response( + git_smart_subtransport_stream *s, + char *buffer, + size_t buffer_size, + size_t *out_len) +{ + http_stream *stream = (http_stream *)s; + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_http_client *client = transport->http_client; + git_http_response response = {0}; + bool complete; + int error; - /* Is the buffer full? If so, then flush */ - if (CHUNK_SIZE == s->chunk_buffer_len) { - if (write_chunk(t->server.stream, - s->chunk_buffer, s->chunk_buffer_len) < 0) - return -1; + *out_len = 0; - s->chunk_buffer_len = 0; + if (stream->state == HTTP_STATE_SENDING_REQUEST) { + if ((error = git_http_client_read_response(&response, client)) < 0 || + (error = handle_response(&complete, stream, &response, false)) < 0) + goto done; - if (len > 0) { - memcpy(s->chunk_buffer, buffer, len); - s->chunk_buffer_len = len; - } - } + assert(complete); + stream->state = HTTP_STATE_RECEIVING_RESPONSE; } - return 0; -} - -static int http_stream_write_single( - git_smart_subtransport_stream *stream, - const char *buffer, - size_t len) -{ - http_stream *s = GIT_CONTAINER_OF(stream, http_stream, parent); - http_subtransport *t = OWNING_SUBTRANSPORT(s); - git_buf request = GIT_BUF_INIT; - - assert(t->connected); + error = git_http_client_read_body(client, buffer, buffer_size); - if (s->sent_request) { - git_error_set(GIT_ERROR_NET, "subtransport configured for only one write"); - return -1; + if (error > 0) { + *out_len = error; + error = 0; } - clear_parser_state(t); - - if (gen_request(&request, s, len) < 0) - return -1; - - if (git_stream__write_full(t->server.stream, request.ptr, request.size, 0) < 0) - goto on_error; - - if (len && git_stream__write_full(t->server.stream, buffer, len, 0) < 0) - goto on_error; - - git_buf_dispose(&request); - s->sent_request = 1; - - return 0; - -on_error: - git_buf_dispose(&request); - return -1; +done: + git_http_response_dispose(&response); + return error; } static void http_stream_free(git_smart_subtransport_stream *stream) { http_stream *s = GIT_CONTAINER_OF(stream, http_stream, parent); - - if (s->chunk_buffer) - git__free(s->chunk_buffer); - - if (s->redirect_url) - git__free(s->redirect_url); - git__free(s); } -static int http_stream_alloc(http_subtransport *t, - git_smart_subtransport_stream **stream) -{ - http_stream *s; - - if (!stream) - return -1; - - s = git__calloc(sizeof(http_stream), 1); - GIT_ERROR_CHECK_ALLOC(s); - - s->parent.subtransport = &t->parent; - s->parent.read = http_stream_read; - s->parent.write = http_stream_write_single; - s->parent.free = http_stream_free; - - *stream = (git_smart_subtransport_stream *)s; - return 0; -} - -static int http_uploadpack_ls( - http_subtransport *t, - git_smart_subtransport_stream **stream) +static const http_service *select_service(git_smart_service_t action) { - http_stream *s; - - if (http_stream_alloc(t, stream) < 0) - return -1; - - s = (http_stream *)*stream; - - s->service = upload_pack_service; - s->service_url = upload_pack_ls_service_url; - s->verb = get_verb; - - return 0; -} - -static int http_uploadpack( - http_subtransport *t, - git_smart_subtransport_stream **stream) -{ - http_stream *s; - - if (http_stream_alloc(t, stream) < 0) - return -1; - - s = (http_stream *)*stream; - - s->service = upload_pack_service; - s->service_url = upload_pack_service_url; - s->verb = post_verb; - - return 0; -} - -static int http_receivepack_ls( - http_subtransport *t, - git_smart_subtransport_stream **stream) -{ - http_stream *s; - - if (http_stream_alloc(t, stream) < 0) - return -1; - - s = (http_stream *)*stream; - - s->service = receive_pack_service; - s->service_url = receive_pack_ls_service_url; - s->verb = get_verb; - - return 0; -} - -static int http_receivepack( - http_subtransport *t, - git_smart_subtransport_stream **stream) -{ - http_stream *s; - - if (http_stream_alloc(t, stream) < 0) - return -1; - - s = (http_stream *)*stream; - - /* Use Transfer-Encoding: chunked for this request */ - s->chunked = 1; - s->parent.write = http_stream_write_chunked; - - s->service = receive_pack_service; - s->service_url = receive_pack_service_url; - s->verb = post_verb; + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return &upload_pack_ls_service; + case GIT_SERVICE_UPLOADPACK: + return &upload_pack_service; + case GIT_SERVICE_RECEIVEPACK_LS: + return &receive_pack_ls_service; + case GIT_SERVICE_RECEIVEPACK: + return &receive_pack_service; + } - return 0; + return NULL; } static int http_action( - git_smart_subtransport_stream **stream, - git_smart_subtransport *subtransport, + git_smart_subtransport_stream **out, + git_smart_subtransport *t, const char *url, git_smart_service_t action) { - http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent); - int ret; + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); + http_stream *stream; + const http_service *service; + int error; + + assert(out && t); - assert(stream); + *out = NULL; /* * If we've seen a redirect then preserve the location that we've @@ -1542,103 +648,89 @@ static int http_action( * have redirected us from HTTP->HTTPS and is using an auth mechanism * that would be insecure in plaintext (eg, HTTP Basic). */ - if ((!t->server.url.host || !t->server.url.port || !t->server.url.path) && - (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); + if (!git_net_url_valid(&transport->server.url) && + (error = git_net_url_parse(&transport->server.url, url)) < 0) + return error; - if ((ret = http_connect(t)) < 0) - return ret; + if ((service = select_service(action)) == NULL) { + git_error_set(GIT_ERROR_HTTP, "invalid action"); + return -1; + } - switch (action) { - case GIT_SERVICE_UPLOADPACK_LS: - return http_uploadpack_ls(t, stream); + stream = git__calloc(sizeof(http_stream), 1); + GIT_ERROR_CHECK_ALLOC(stream); - case GIT_SERVICE_UPLOADPACK: - return http_uploadpack(t, stream); + if (!transport->http_client) { + git_http_client_options opts = {0}; - case GIT_SERVICE_RECEIVEPACK_LS: - return http_receivepack_ls(t, stream); + opts.server_certificate_check_cb = transport->owner->certificate_check_cb; + opts.server_certificate_check_payload = transport->owner->message_cb_payload; + opts.proxy_certificate_check_cb = transport->owner->proxy.certificate_check; + opts.proxy_certificate_check_payload = transport->owner->proxy.payload; - case GIT_SERVICE_RECEIVEPACK: - return http_receivepack(t, stream); + if (git_http_client_new(&transport->http_client, &opts) < 0) + return -1; } - *stream = NULL; - return -1; -} - -static int http_close(git_smart_subtransport *subtransport) -{ - http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent); - - clear_parser_state(t); + stream->service = service; + stream->parent.subtransport = &transport->parent; - t->connected = 0; - - if (t->server.stream) { - git_stream_close(t->server.stream); - git_stream_free(t->server.stream); - t->server.stream = NULL; + if (service->method == GIT_HTTP_METHOD_GET) { + stream->parent.read = http_stream_read; + } else { + stream->parent.write = http_stream_write; + stream->parent.read = http_stream_read_response; } - if (t->proxy.stream) { - git_stream_close(t->proxy.stream); - git_stream_free(t->proxy.stream); - t->proxy.stream = NULL; - } + stream->parent.free = http_stream_free; - free_cred(&t->server.cred); - free_cred(&t->proxy.cred); + *out = (git_smart_subtransport_stream *)stream; + return 0; +} - free_auth_context(&t->server); - free_auth_context(&t->proxy); +static int http_close(git_smart_subtransport *t) +{ + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); - t->server.url_cred_presented = false; - t->proxy.url_cred_presented = false; + free_cred(&transport->server.cred); + free_cred(&transport->proxy.cred); - git_net_url_dispose(&t->server.url); - git_net_url_dispose(&t->proxy.url); + transport->server.url_cred_presented = false; + transport->proxy.url_cred_presented = false; - git__free(t->proxy_url); - t->proxy_url = NULL; + git_net_url_dispose(&transport->server.url); + git_net_url_dispose(&transport->proxy.url); return 0; } -static void http_free(git_smart_subtransport *subtransport) +static void http_free(git_smart_subtransport *t) { - http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent); + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); - http_close(subtransport); - git__free(t); + git_http_client_free(transport->http_client); + + http_close(t); + git__free(transport); } int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner, void *param) { - http_subtransport *t; + http_subtransport *transport; GIT_UNUSED(param); - if (!out) - return -1; - - t = git__calloc(sizeof(http_subtransport), 1); - GIT_ERROR_CHECK_ALLOC(t); + assert(out); - t->owner = (transport_smart *)owner; - t->parent.action = http_action; - t->parent.close = http_close; - t->parent.free = http_free; + transport = git__calloc(sizeof(http_subtransport), 1); + GIT_ERROR_CHECK_ALLOC(transport); - t->settings.on_header_field = on_header_field; - t->settings.on_header_value = on_header_value; - t->settings.on_headers_complete = on_headers_complete; - t->settings.on_body = on_body_fill_buffer; - t->settings.on_message_complete = on_message_complete; + transport->owner = (transport_smart *)owner; + transport->parent.action = http_action; + transport->parent.close = http_close; + transport->parent.free = http_free; - *out = (git_smart_subtransport *) t; + *out = (git_smart_subtransport *) transport; return 0; } diff --git a/src/transports/http.h b/src/transports/http.h index ddaab0b45..c02109cec 100644 --- a/src/transports/http.h +++ b/src/transports/http.h @@ -9,9 +9,12 @@ #define INCLUDE_transports_http_h__ #include "buffer.h" +#include "httpclient.h" #define GIT_HTTP_REPLAY_MAX 15 +extern bool git_http__expect_continue; + GIT_INLINE(int) git_http__user_agent(git_buf *buf) { const char *ua = git_libgit2__user_agent(); diff --git a/src/transports/httpclient.c b/src/transports/httpclient.c new file mode 100644 index 000000000..7f44a26dc --- /dev/null +++ b/src/transports/httpclient.c @@ -0,0 +1,1526 @@ +/* + * 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 "common.h" +#include "git2.h" +#include "http_parser.h" +#include "vector.h" +#include "trace.h" +#include "global.h" +#include "httpclient.h" +#include "http.h" +#include "auth.h" +#include "auth_negotiate.h" +#include "auth_ntlm.h" +#include "git2/sys/cred.h" +#include "net.h" +#include "stream.h" +#include "streams/socket.h" +#include "streams/tls.h" +#include "auth.h" + +static git_http_auth_scheme auth_schemes[] = { + { GIT_HTTP_AUTH_NEGOTIATE, "Negotiate", GIT_CREDTYPE_DEFAULT, git_http_auth_negotiate }, + { GIT_HTTP_AUTH_NTLM, "NTLM", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_ntlm }, + { GIT_HTTP_AUTH_BASIC, "Basic", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_basic }, +}; + +#define GIT_READ_BUFFER_SIZE 8192 + +typedef struct { + git_net_url url; + git_stream *stream; + + git_vector auth_challenges; + git_http_auth_context *auth_context; +} git_http_server; + +typedef enum { + PROXY = 1, + SERVER +} git_http_server_t; + +typedef enum { + NONE = 0, + SENDING_REQUEST, + SENDING_BODY, + SENT_REQUEST, + HAS_EARLY_RESPONSE, + READING_RESPONSE, + READING_BODY, + DONE +} http_client_state; + +/* Parser state */ +typedef enum { + PARSE_HEADER_NONE = 0, + PARSE_HEADER_NAME, + PARSE_HEADER_VALUE, + PARSE_HEADER_COMPLETE +} parse_header_state; + +typedef enum { + PARSE_STATUS_OK, + PARSE_STATUS_NO_OUTPUT, + PARSE_STATUS_ERROR +} parse_status; + +typedef struct { + git_http_client *client; + git_http_response *response; + + /* Temporary buffers to avoid extra mallocs */ + git_buf parse_header_name; + git_buf parse_header_value; + + /* Parser state */ + int error; + parse_status parse_status; + + /* Headers parsing */ + parse_header_state parse_header_state; + + /* Body parsing */ + char *output_buf; /* Caller's output buffer */ + size_t output_size; /* Size of caller's output buffer */ + size_t output_written; /* Bytes we've written to output buffer */ +} http_parser_context; + +/* HTTP client connection */ +struct git_http_client { + git_http_client_options opts; + + /* Are we writing to the proxy or server, and state of the client. */ + git_http_server_t current_server; + http_client_state state; + + http_parser parser; + + git_http_server server; + git_http_server proxy; + + unsigned request_count; + unsigned connected : 1, + proxy_connected : 1, + keepalive : 1, + request_chunked : 1; + + /* Temporary buffers to avoid extra mallocs */ + git_buf request_msg; + git_buf read_buf; + + /* A subset of information from the request */ + size_t request_body_len, + request_body_remain; + + /* + * When state == HAS_EARLY_RESPONSE, the response of our proxy + * that we have buffered and will deliver during read_response. + */ + git_http_response early_response; +}; + +bool git_http_response_is_redirect(git_http_response *response) +{ + return (response->status == GIT_HTTP_MOVED_PERMANENTLY || + response->status == GIT_HTTP_FOUND || + response->status == GIT_HTTP_SEE_OTHER || + response->status == GIT_HTTP_TEMPORARY_REDIRECT || + response->status == GIT_HTTP_PERMANENT_REDIRECT); +} + +void git_http_response_dispose(git_http_response *response) +{ + assert(response); + + git__free(response->content_type); + git__free(response->location); + + memset(response, 0, sizeof(git_http_response)); +} + +static int on_header_complete(http_parser *parser) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + git_http_client *client = ctx->client; + git_http_response *response = ctx->response; + + git_buf *name = &ctx->parse_header_name; + git_buf *value = &ctx->parse_header_value; + + if (!strcasecmp("Content-Type", name->ptr)) { + if (response->content_type) { + git_error_set(GIT_ERROR_HTTP, + "multiple content-type headers"); + return -1; + } + + response->content_type = + git__strndup(value->ptr, value->size); + GIT_ERROR_CHECK_ALLOC(ctx->response->content_type); + } else if (!strcasecmp("Content-Length", name->ptr)) { + int64_t len; + + if (response->content_length) { + git_error_set(GIT_ERROR_HTTP, + "multiple content-length headers"); + return -1; + } + + if (git__strntol64(&len, value->ptr, value->size, + NULL, 10) < 0 || len < 0) { + git_error_set(GIT_ERROR_HTTP, + "invalid content-length"); + return -1; + } + + response->content_length = (size_t)len; + } else if (!strcasecmp("Transfer-Encoding", name->ptr) && + !strcasecmp("chunked", value->ptr)) { + ctx->response->chunked = 1; + } else if (!strcasecmp("Proxy-Authenticate", git_buf_cstr(name))) { + char *dup = git__strndup(value->ptr, value->size); + GIT_ERROR_CHECK_ALLOC(dup); + + if (git_vector_insert(&client->proxy.auth_challenges, dup) < 0) + return -1; + } else if (!strcasecmp("WWW-Authenticate", name->ptr)) { + char *dup = git__strndup(value->ptr, value->size); + GIT_ERROR_CHECK_ALLOC(dup); + + if (git_vector_insert(&client->server.auth_challenges, dup) < 0) + return -1; + } else if (!strcasecmp("Location", name->ptr)) { + if (response->location) { + git_error_set(GIT_ERROR_HTTP, + "multiple location headers"); + return -1; + } + + response->location = git__strndup(value->ptr, value->size); + GIT_ERROR_CHECK_ALLOC(response->location); + } + + return 0; +} + +static int on_header_field(http_parser *parser, const char *str, size_t len) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + + switch (ctx->parse_header_state) { + /* + * We last saw a header value, process the name/value pair and + * get ready to handle this new name. + */ + case PARSE_HEADER_VALUE: + if (on_header_complete(parser) < 0) + return ctx->parse_status = PARSE_STATUS_ERROR; + + git_buf_clear(&ctx->parse_header_name); + git_buf_clear(&ctx->parse_header_value); + /* Fall through */ + + case PARSE_HEADER_NONE: + case PARSE_HEADER_NAME: + ctx->parse_header_state = PARSE_HEADER_NAME; + + if (git_buf_put(&ctx->parse_header_name, str, len) < 0) + return ctx->parse_status = PARSE_STATUS_ERROR; + + break; + + default: + git_error_set(GIT_ERROR_HTTP, + "header name seen at unexpected time"); + return ctx->parse_status = PARSE_STATUS_ERROR; + } + + return 0; +} + +static int on_header_value(http_parser *parser, const char *str, size_t len) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + + switch (ctx->parse_header_state) { + case PARSE_HEADER_NAME: + case PARSE_HEADER_VALUE: + ctx->parse_header_state = PARSE_HEADER_VALUE; + + if (git_buf_put(&ctx->parse_header_value, str, len) < 0) + return ctx->parse_status = PARSE_STATUS_ERROR; + + break; + + default: + git_error_set(GIT_ERROR_HTTP, + "header value seen at unexpected time"); + return ctx->parse_status = PARSE_STATUS_ERROR; + } + + return 0; +} + +GIT_INLINE(bool) challenge_matches_scheme( + const char *challenge, + git_http_auth_scheme *scheme) +{ + const char *scheme_name = scheme->name; + size_t scheme_len = strlen(scheme_name); + + if (!strncasecmp(challenge, scheme_name, scheme_len) && + (challenge[scheme_len] == '\0' || challenge[scheme_len] == ' ')) + return true; + + return false; +} + +static git_http_auth_scheme *scheme_for_challenge(const char *challenge) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { + if (challenge_matches_scheme(challenge, &auth_schemes[i])) + return &auth_schemes[i]; + } + + return NULL; +} + +GIT_INLINE(void) collect_authinfo( + unsigned int *schemetypes, + unsigned int *credtypes, + git_vector *challenges) +{ + git_http_auth_scheme *scheme; + const char *challenge; + size_t i; + + *schemetypes = 0; + *credtypes = 0; + + git_vector_foreach(challenges, i, challenge) { + if ((scheme = scheme_for_challenge(challenge)) != NULL) { + *schemetypes |= scheme->type; + *credtypes |= scheme->credtypes; + } + } +} + +static int resend_needed(git_http_client *client, git_http_response *response) +{ + git_http_auth_context *auth_context; + + if (response->status == GIT_HTTP_STATUS_UNAUTHORIZED && + (auth_context = client->server.auth_context) && + auth_context->is_complete && + !auth_context->is_complete(auth_context)) + return 1; + + if (response->status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED && + (auth_context = client->proxy.auth_context) && + auth_context->is_complete && + !auth_context->is_complete(auth_context)) + return 1; + + return 0; +} + +static int on_headers_complete(http_parser *parser) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + + /* Finalize the last seen header */ + switch (ctx->parse_header_state) { + case PARSE_HEADER_VALUE: + if (on_header_complete(parser) < 0) + return ctx->parse_status = PARSE_STATUS_ERROR; + + /* Fall through */ + + case PARSE_HEADER_NONE: + ctx->parse_header_state = PARSE_HEADER_COMPLETE; + break; + + default: + git_error_set(GIT_ERROR_HTTP, + "header completion at unexpected time"); + return ctx->parse_status = PARSE_STATUS_ERROR; + } + + ctx->response->status = parser->status_code; + ctx->client->keepalive = http_should_keep_alive(parser); + + /* Prepare for authentication */ + collect_authinfo(&ctx->response->server_auth_schemetypes, + &ctx->response->server_auth_credtypes, + &ctx->client->server.auth_challenges); + collect_authinfo(&ctx->response->proxy_auth_schemetypes, + &ctx->response->proxy_auth_credtypes, + &ctx->client->proxy.auth_challenges); + + ctx->response->resend_credentials = resend_needed(ctx->client, + ctx->response); + + /* Stop parsing. */ + http_parser_pause(parser, 1); + + if (ctx->response->content_type || ctx->response->chunked) + ctx->client->state = READING_BODY; + else + ctx->client->state = DONE; + + return 0; +} + +static int on_body(http_parser *parser, const char *buf, size_t len) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + size_t max_len; + + /* Saw data when we expected not to (eg, in consume_response_body) */ + if (ctx->output_buf == NULL && ctx->output_size == 0) { + ctx->parse_status = PARSE_STATUS_NO_OUTPUT; + return 0; + } + + assert(ctx->output_size >= ctx->output_written); + + max_len = min(ctx->output_size - ctx->output_written, len); + max_len = min(max_len, INT_MAX); + + memcpy(ctx->output_buf + ctx->output_written, buf, max_len); + ctx->output_written += max_len; + + return 0; +} + +static int on_message_complete(http_parser *parser) +{ + http_parser_context *ctx = (http_parser_context *) parser->data; + + ctx->client->state = DONE; + return 0; +} + +GIT_INLINE(int) stream_write( + git_http_server *server, + const char *data, + size_t len) +{ + git_trace(GIT_TRACE_TRACE, + "Sending request:\n%.*s", (int)len, data); + + return git_stream__write_full(server->stream, data, len, 0); +} + +GIT_INLINE(int) client_write_request(git_http_client *client) +{ + git_stream *stream = client->current_server == PROXY ? + client->proxy.stream : client->server.stream; + + git_trace(GIT_TRACE_TRACE, + "Sending request:\n%.*s", + (int)client->request_msg.size, client->request_msg.ptr); + + return git_stream__write_full(stream, + client->request_msg.ptr, + client->request_msg.size, + 0); +} + +const char *name_for_method(git_http_method method) +{ + switch (method) { + case GIT_HTTP_METHOD_GET: + return "GET"; + case GIT_HTTP_METHOD_POST: + return "POST"; + case GIT_HTTP_METHOD_CONNECT: + return "CONNECT"; + } + + return NULL; +} + +/* + * Find the scheme that is suitable for the given credentials, based on the + * server's auth challenges. + */ +static bool best_scheme_and_challenge( + git_http_auth_scheme **scheme_out, + const char **challenge_out, + git_vector *challenges, + git_cred *credentials) +{ + const char *challenge; + size_t i, j; + + for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { + git_vector_foreach(challenges, j, challenge) { + git_http_auth_scheme *scheme = &auth_schemes[i]; + + if (challenge_matches_scheme(challenge, scheme) && + (scheme->credtypes & credentials->credtype)) { + *scheme_out = scheme; + *challenge_out = challenge; + return true; + } + } + } + + return false; +} + +/* + * Find the challenge from the server for our current auth context. + */ +static const char *challenge_for_context( + git_vector *challenges, + git_http_auth_context *auth_ctx) +{ + const char *challenge; + size_t i, j; + + for (i = 0; i < ARRAY_SIZE(auth_schemes); i++) { + if (auth_schemes[i].type == auth_ctx->type) { + git_http_auth_scheme *scheme = &auth_schemes[i]; + + git_vector_foreach(challenges, j, challenge) { + if (challenge_matches_scheme(challenge, scheme)) + return challenge; + } + } + } + + return NULL; +} + +static const char *init_auth_context( + git_http_server *server, + git_vector *challenges, + git_cred *credentials) +{ + git_http_auth_scheme *scheme; + const char *challenge; + int error; + + if (!best_scheme_and_challenge(&scheme, &challenge, challenges, credentials)) { + git_error_set(GIT_ERROR_HTTP, "could not find appropriate mechanism for credentials"); + return NULL; + } + + error = scheme->init_context(&server->auth_context, &server->url); + + if (error == GIT_PASSTHROUGH) { + git_error_set(GIT_ERROR_HTTP, "'%s' authentication is not supported", scheme->name); + return NULL; + } + + return challenge; +} + +static void free_auth_context(git_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 apply_credentials( + git_buf *buf, + git_http_server *server, + const char *header_name, + git_cred *credentials) +{ + git_http_auth_context *auth = server->auth_context; + git_vector *challenges = &server->auth_challenges; + const char *challenge; + git_buf token = GIT_BUF_INIT; + int error = 0; + + /* We've started a new request without creds; free the context. */ + if (auth && !credentials) { + free_auth_context(server); + return 0; + } + + /* We haven't authenticated, nor were we asked to. Nothing to do. */ + if (!auth && !git_vector_length(challenges)) + return 0; + + if (!auth) { + challenge = init_auth_context(server, challenges, credentials); + auth = server->auth_context; + + if (!challenge || !auth) { + error = -1; + goto done; + } + } else if (auth->set_challenge) { + challenge = challenge_for_context(challenges, auth); + } + + if (auth->set_challenge && challenge && + (error = auth->set_challenge(auth, challenge)) < 0) + goto done; + + if ((error = auth->next_token(&token, auth, credentials)) < 0) + goto done; + + if (auth->is_complete && auth->is_complete(auth)) { + /* + * If we're done with an auth mechanism with connection affinity, + * we don't need to send any more headers and can dispose the context. + */ + if (auth->connection_affinity) + free_auth_context(server); + } else if (!token.size) { + git_error_set(GIT_ERROR_HTTP, "failed to respond to authentication challange"); + error = -1; + goto done; + } + + if (token.size > 0) + error = git_buf_printf(buf, "%s: %s\r\n", header_name, token.ptr); + +done: + git_buf_dispose(&token); + return error; +} + +GIT_INLINE(int) apply_server_credentials( + git_buf *buf, + git_http_client *client, + git_http_request *request) +{ + return apply_credentials(buf, + &client->server, + "Authorization", + request->credentials); +} + +GIT_INLINE(int) apply_proxy_credentials( + git_buf *buf, + git_http_client *client, + git_http_request *request) +{ + return apply_credentials(buf, + &client->proxy, + "Proxy-Authorization", + request->proxy_credentials); +} + +static int generate_connect_request( + git_http_client *client, + git_http_request *request) +{ + git_buf *buf; + int error; + + git_buf_clear(&client->request_msg); + buf = &client->request_msg; + + git_buf_printf(buf, "CONNECT %s:%s HTTP/1.1\r\n", + client->server.url.host, client->server.url.port); + + git_buf_puts(buf, "User-Agent: "); + git_http__user_agent(buf); + git_buf_puts(buf, "\r\n"); + + git_buf_printf(buf, "Host: %s\r\n", client->proxy.url.host); + + if ((error = apply_proxy_credentials(buf, client, request) < 0)) + return -1; + + git_buf_puts(buf, "\r\n"); + + return git_buf_oom(buf) ? -1 : 0; +} + +static int generate_request( + git_http_client *client, + git_http_request *request) +{ + git_buf *buf; + size_t i; + int error; + + assert(client && request); + + git_buf_clear(&client->request_msg); + buf = &client->request_msg; + + /* GET|POST path HTTP/1.1 */ + git_buf_puts(buf, name_for_method(request->method)); + git_buf_putc(buf, ' '); + + if (request->proxy && strcmp(request->url->scheme, "https")) + git_net_url_fmt(buf, request->url); + else + git_net_url_fmt_path(buf, request->url); + + git_buf_puts(buf, " HTTP/1.1\r\n"); + + git_buf_puts(buf, "User-Agent: "); + git_http__user_agent(buf); + git_buf_puts(buf, "\r\n"); + + git_buf_printf(buf, "Host: %s", request->url->host); + + if (!git_net_url_is_default_port(request->url)) + git_buf_printf(buf, ":%s", request->url->port); + + git_buf_puts(buf, "\r\n"); + + if (request->accept) + git_buf_printf(buf, "Accept: %s\r\n", request->accept); + else + git_buf_puts(buf, "Accept: */*\r\n"); + + if (request->content_type) + git_buf_printf(buf, "Content-Type: %s\r\n", + request->content_type); + + if (request->chunked) + git_buf_puts(buf, "Transfer-Encoding: chunked\r\n"); + + if (request->content_length > 0) + git_buf_printf(buf, "Content-Length: %"PRIuZ "\r\n", + request->content_length); + + if (request->expect_continue) + git_buf_printf(buf, "Expect: 100-continue\r\n"); + + if ((error = apply_server_credentials(buf, client, request)) < 0 || + (error = apply_proxy_credentials(buf, client, request)) < 0) + return error; + + if (request->custom_headers) { + for (i = 0; i < request->custom_headers->count; i++) { + const char *hdr = request->custom_headers->strings[i]; + + if (hdr) + git_buf_printf(buf, "%s\r\n", hdr); + } + } + + git_buf_puts(buf, "\r\n"); + + if (git_buf_oom(buf)) + return -1; + + return 0; +} + +static int check_certificate( + git_stream *stream, + git_net_url *url, + int is_valid, + git_transport_certificate_check_cb cert_cb, + void *cert_cb_payload) +{ + git_cert *cert; + git_error_state last_error = {0}; + int error; + + if ((error = git_stream_certificate(&cert, stream)) < 0) + return error; + + git_error_state_capture(&last_error, GIT_ECERTIFICATE); + + error = cert_cb(cert, is_valid, url->host, cert_cb_payload); + + if (error == GIT_PASSTHROUGH && !is_valid) + return git_error_state_restore(&last_error); + else if (error == GIT_PASSTHROUGH) + error = 0; + else if (error && !git_error_last()) + git_error_set(GIT_ERROR_HTTP, + "user rejected certificate for %s", url->host); + + git_error_state_free(&last_error); + return error; +} + +static int server_connect_stream( + git_http_server *server, + git_transport_certificate_check_cb cert_cb, + void *cb_payload) +{ + int error; + + GIT_ERROR_CHECK_VERSION(server->stream, GIT_STREAM_VERSION, "git_stream"); + + error = git_stream_connect(server->stream); + + if (error && error != GIT_ECERTIFICATE) + return error; + + if (git_stream_is_encrypted(server->stream) && cert_cb != NULL) + error = check_certificate(server->stream, &server->url, !error, + cert_cb, cb_payload); + + return error; +} + +static void reset_auth_connection(git_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->auth_context && + server->auth_context->connection_affinity) + free_auth_context(server); +} + +/* + * Updates the server data structure with the new URL; returns 1 if the server + * has changed and we need to reconnect, returns 0 otherwise. + */ +GIT_INLINE(int) server_setup_from_url( + git_http_server *server, + git_net_url *url) +{ + if (!server->url.scheme || strcmp(server->url.scheme, url->scheme) || + !server->url.host || strcmp(server->url.host, url->host) || + !server->url.port || strcmp(server->url.port, url->port)) { + git__free(server->url.scheme); + git__free(server->url.host); + git__free(server->url.port); + + server->url.scheme = git__strdup(url->scheme); + GIT_ERROR_CHECK_ALLOC(server->url.scheme); + + server->url.host = git__strdup(url->host); + GIT_ERROR_CHECK_ALLOC(server->url.host); + + server->url.port = git__strdup(url->port); + GIT_ERROR_CHECK_ALLOC(server->url.port); + + return 1; + } + + return 0; +} + +static void reset_parser(git_http_client *client) +{ + http_parser_init(&client->parser, HTTP_RESPONSE); +} + +static int setup_hosts( + git_http_client *client, + git_http_request *request) +{ + int ret, diff = 0; + + assert(client && request && request->url); + + if ((ret = server_setup_from_url(&client->server, request->url)) < 0) + return ret; + + diff |= ret; + + if (request->proxy && + (ret = server_setup_from_url(&client->proxy, request->proxy)) < 0) + return ret; + + diff |= ret; + + if (diff) { + free_auth_context(&client->server); + free_auth_context(&client->proxy); + + client->connected = 0; + } + + return 0; +} + +GIT_INLINE(int) server_create_stream(git_http_server *server) +{ + git_net_url *url = &server->url; + + if (strcasecmp(url->scheme, "https") == 0) + return git_tls_stream_new(&server->stream, url->host, url->port); + else if (strcasecmp(url->scheme, "http") == 0) + return git_socket_stream_new(&server->stream, url->host, url->port); + + git_error_set(GIT_ERROR_HTTP, "unknown http scheme '%s'", url->scheme); + return -1; +} + +GIT_INLINE(void) save_early_response( + git_http_client *client, + git_http_response *response) +{ + /* Buffer the response so we can return it in read_response */ + client->state = HAS_EARLY_RESPONSE; + + memcpy(&client->early_response, response, sizeof(git_http_response)); + memset(response, 0, sizeof(git_http_response)); +} + +static int proxy_connect( + git_http_client *client, + git_http_request *request) +{ + git_http_response response = {0}; + int error; + + if (!client->proxy_connected || !client->keepalive) { + git_trace(GIT_TRACE_DEBUG, "Connecting to proxy %s:%s", + client->proxy.url.host, client->proxy.url.port); + + if ((error = server_create_stream(&client->proxy)) < 0 || + (error = server_connect_stream(&client->proxy, + client->opts.proxy_certificate_check_cb, + client->opts.proxy_certificate_check_payload)) < 0) + goto done; + + client->proxy_connected = 1; + } + + client->current_server = PROXY; + client->state = SENDING_REQUEST; + + if ((error = generate_connect_request(client, request)) < 0 || + (error = client_write_request(client)) < 0) + goto done; + + client->state = SENT_REQUEST; + + if ((error = git_http_client_read_response(&response, client)) < 0 || + (error = git_http_client_skip_body(client)) < 0) + goto done; + + assert(client->state == DONE); + + if (response.status == GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED) { + save_early_response(client, &response); + + error = GIT_RETRY; + goto done; + } else if (response.status != GIT_HTTP_STATUS_OK) { + git_error_set(GIT_ERROR_HTTP, "proxy returned unexpected status: %d", response.status); + error = -1; + goto done; + } + + reset_parser(client); + client->state = NONE; + +done: + git_http_response_dispose(&response); + return error; +} + +static int server_connect(git_http_client *client) +{ + git_net_url *url = &client->server.url; + git_transport_certificate_check_cb cert_cb; + void *cert_payload; + int error; + + client->current_server = SERVER; + + if (client->proxy.stream) + error = git_tls_stream_wrap(&client->server.stream, client->proxy.stream, url->host); + else + error = server_create_stream(&client->server); + + if (error < 0) + goto done; + + cert_cb = client->opts.server_certificate_check_cb; + cert_payload = client->opts.server_certificate_check_payload; + + error = server_connect_stream(&client->server, cert_cb, cert_payload); + +done: + return error; +} + +GIT_INLINE(void) close_stream(git_http_server *server) +{ + if (server->stream) { + git_stream_close(server->stream); + git_stream_free(server->stream); + server->stream = NULL; + } +} + +static int http_client_connect( + git_http_client *client, + git_http_request *request) +{ + bool use_proxy = false; + int error; + + if ((error = setup_hosts(client, request)) < 0) + goto on_error; + + /* We're connected to our destination server; no need to reconnect */ + if (client->connected && client->keepalive && + (client->state == NONE || client->state == DONE)) + return 0; + + client->connected = 0; + client->request_count = 0; + + close_stream(&client->server); + reset_auth_connection(&client->server); + + reset_parser(client); + + /* Reconnect to the proxy if necessary. */ + use_proxy = client->proxy.url.host && + !strcmp(client->server.url.scheme, "https"); + + if (use_proxy) { + if (!client->proxy_connected || !client->keepalive || + (client->state != NONE && client->state != DONE)) { + close_stream(&client->proxy); + reset_auth_connection(&client->proxy); + + client->proxy_connected = 0; + } + + if ((error = proxy_connect(client, request)) < 0) + goto on_error; + } + + git_trace(GIT_TRACE_DEBUG, "Connecting to remote %s:%s", + client->server.url.host, client->server.url.port); + + if ((error = server_connect(client)) < 0) + goto on_error; + + client->connected = 1; + return error; + +on_error: + if (error != GIT_RETRY) + close_stream(&client->proxy); + + close_stream(&client->server); + return error; +} + +GIT_INLINE(int) client_read(git_http_client *client) +{ + git_stream *stream; + char *buf = client->read_buf.ptr + client->read_buf.size; + size_t max_len; + ssize_t read_len; + + stream = client->current_server == PROXY ? + client->proxy.stream : client->server.stream; + + /* + * We use a git_buf for convenience, but statically allocate it and + * don't resize. Limit our consumption to INT_MAX since calling + * functions use an int return type to return number of bytes read. + */ + max_len = client->read_buf.asize - client->read_buf.size; + max_len = min(max_len, INT_MAX); + + if (max_len == 0) { + git_error_set(GIT_ERROR_HTTP, "no room in output buffer"); + return -1; + } + + read_len = git_stream_read(stream, buf, max_len); + + if (read_len >= 0) { + client->read_buf.size += read_len; + + git_trace(GIT_TRACE_TRACE, "Received:\n%.*s", + (int)read_len, buf); + } + + return (int)read_len; +} + +static bool parser_settings_initialized; +static http_parser_settings parser_settings; + +GIT_INLINE(http_parser_settings *) http_client_parser_settings(void) +{ + if (!parser_settings_initialized) { + parser_settings.on_header_field = on_header_field; + parser_settings.on_header_value = on_header_value; + parser_settings.on_headers_complete = on_headers_complete; + parser_settings.on_body = on_body; + parser_settings.on_message_complete = on_message_complete; + + parser_settings_initialized = true; + } + + return &parser_settings; +} + +GIT_INLINE(int) client_read_and_parse(git_http_client *client) +{ + http_parser *parser = &client->parser; + http_parser_context *ctx = (http_parser_context *) parser->data; + unsigned char http_errno; + int read_len; + size_t parsed_len; + + /* + * If we have data in our read buffer, that means we stopped early + * when parsing headers. Use the data in the read buffer instead of + * reading more from the socket. + */ + if (!client->read_buf.size && (read_len = client_read(client)) < 0) + return read_len; + + parsed_len = http_parser_execute(parser, + http_client_parser_settings(), + client->read_buf.ptr, + client->read_buf.size); + http_errno = client->parser.http_errno; + + if (parsed_len > INT_MAX) { + git_error_set(GIT_ERROR_HTTP, "unexpectedly large parse"); + return -1; + } + + if (parser->upgrade) { + git_error_set(GIT_ERROR_HTTP, "server requested upgrade"); + return -1; + } + + if (ctx->parse_status == PARSE_STATUS_ERROR) { + client->connected = 0; + return ctx->error ? ctx->error : -1; + } + + /* + * If we finished reading the headers or body, we paused parsing. + * Otherwise the parser will start filling the body, or even parse + * a new response if the server pipelined us multiple responses. + * (This can happen in response to an expect/continue request, + * where the server gives you a 100 and 200 simultaneously.) + */ + if (http_errno == HPE_PAUSED) { + /* + * http-parser has a "feature" where it will not deliver the + * final byte when paused in a callback. Consume that byte. + * https://github.com/nodejs/http-parser/issues/97 + */ + assert(client->read_buf.size > parsed_len); + + http_parser_pause(parser, 0); + + parsed_len += http_parser_execute(parser, + http_client_parser_settings(), + client->read_buf.ptr + parsed_len, + 1); + } + + /* Most failures will be reported in http_errno */ + else if (parser->http_errno != HPE_OK) { + git_error_set(GIT_ERROR_HTTP, "http parser error: %s", + http_errno_description(http_errno)); + return -1; + } + + /* Otherwise we should have consumed the entire buffer. */ + else if (parsed_len != client->read_buf.size) { + git_error_set(GIT_ERROR_HTTP, + "http parser did not consume entire buffer: %s", + http_errno_description(http_errno)); + return -1; + } + + /* recv returned 0, the server hung up on us */ + else if (!parsed_len) { + git_error_set(GIT_ERROR_HTTP, "unexpected EOF"); + return -1; + } + + git_buf_consume_bytes(&client->read_buf, parsed_len); + + return (int)parsed_len; +} + +/* + * See if we've consumed the entire response body. If the client was + * reading the body but did not consume it entirely, it's possible that + * they knew that the stream had finished (in a git response, seeing a + * final flush) and stopped reading. But if the response was chunked, + * we may have not consumed the final chunk marker. Consume it to + * ensure that we don't have it waiting in our socket. If there's + * more than just a chunk marker, close the connection. + */ +static void complete_response_body(git_http_client *client) +{ + http_parser_context parser_context = {0}; + + /* If we're not keeping alive, don't bother. */ + if (!client->keepalive) { + client->connected = 0; + return; + } + + parser_context.client = client; + client->parser.data = &parser_context; + + /* If there was an error, just close the connection. */ + if (client_read_and_parse(client) < 0 || + parser_context.error != HPE_OK || + (parser_context.parse_status != PARSE_STATUS_OK && + parser_context.parse_status != PARSE_STATUS_NO_OUTPUT)) { + git_error_clear(); + client->connected = 0; + } +} + +int git_http_client_send_request( + git_http_client *client, + git_http_request *request) +{ + git_http_response response = {0}; + int error = -1; + + assert(client && request); + + /* If the client did not finish reading, clean up the stream. */ + if (client->state == READING_BODY) + complete_response_body(client); + + /* If we're waiting for proxy auth, don't sending more requests. */ + if (client->state == HAS_EARLY_RESPONSE) + return 0; + + if (git_trace_level() >= GIT_TRACE_DEBUG) { + git_buf url = GIT_BUF_INIT; + git_net_url_fmt(&url, request->url); + git_trace(GIT_TRACE_DEBUG, "Sending %s request to %s", + name_for_method(request->method), + url.ptr ? url.ptr : "<invalid>"); + git_buf_dispose(&url); + } + + if ((error = http_client_connect(client, request)) < 0 || + (error = generate_request(client, request)) < 0 || + (error = client_write_request(client)) < 0) + goto done; + + client->state = SENT_REQUEST; + + if (request->expect_continue) { + if ((error = git_http_client_read_response(&response, client)) < 0 || + (error = git_http_client_skip_body(client)) < 0) + goto done; + + error = 0; + + if (response.status != GIT_HTTP_STATUS_CONTINUE) { + save_early_response(client, &response); + goto done; + } + } + + if (request->content_length || request->chunked) { + client->state = SENDING_BODY; + client->request_body_len = request->content_length; + client->request_body_remain = request->content_length; + client->request_chunked = request->chunked; + } + + reset_parser(client); + +done: + if (error == GIT_RETRY) + error = 0; + + git_http_response_dispose(&response); + return error; +} + +bool git_http_client_has_response(git_http_client *client) +{ + return (client->state == HAS_EARLY_RESPONSE || + client->state > SENT_REQUEST); +} + +int git_http_client_send_body( + git_http_client *client, + const char *buffer, + size_t buffer_len) +{ + git_http_server *server; + git_buf hdr = GIT_BUF_INIT; + int error; + + assert(client); + + /* If we're waiting for proxy auth, don't sending more requests. */ + if (client->state == HAS_EARLY_RESPONSE) + return 0; + + if (client->state != SENDING_BODY) { + git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); + return -1; + } + + if (!buffer_len) + return 0; + + server = &client->server; + + if (client->request_body_len) { + assert(buffer_len <= client->request_body_remain); + + if ((error = stream_write(server, buffer, buffer_len)) < 0) + goto done; + + client->request_body_remain -= buffer_len; + } else { + if ((error = git_buf_printf(&hdr, "%" PRIxZ "\r\n", buffer_len)) < 0 || + (error = stream_write(server, hdr.ptr, hdr.size)) < 0 || + (error = stream_write(server, buffer, buffer_len)) < 0 || + (error = stream_write(server, "\r\n", 2)) < 0) + goto done; + } + +done: + git_buf_dispose(&hdr); + return error; +} + +static int complete_request(git_http_client *client) +{ + int error = 0; + + assert(client && client->state == SENDING_BODY); + + if (client->request_body_len && client->request_body_remain) { + git_error_set(GIT_ERROR_HTTP, "truncated write"); + error = -1; + } else if (client->request_chunked) { + error = stream_write(&client->server, "0\r\n\r\n", 5); + } + + client->state = SENT_REQUEST; + return error; +} + +int git_http_client_read_response( + git_http_response *response, + git_http_client *client) +{ + http_parser_context parser_context = {0}; + int error; + + assert(response && client); + + if (client->state == SENDING_BODY) { + if ((error = complete_request(client)) < 0) + goto done; + } + + if (client->state == HAS_EARLY_RESPONSE) { + memcpy(response, &client->early_response, sizeof(git_http_response)); + memset(&client->early_response, 0, sizeof(git_http_response)); + client->state = DONE; + return 0; + } + + if (client->state != SENT_REQUEST) { + git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); + error = -1; + goto done; + } + + git_http_response_dispose(response); + + git_vector_free_deep(&client->server.auth_challenges); + git_vector_free_deep(&client->proxy.auth_challenges); + + client->state = READING_RESPONSE; + client->keepalive = 0; + client->parser.data = &parser_context; + + parser_context.client = client; + parser_context.response = response; + + while (client->state == READING_RESPONSE) { + if ((error = client_read_and_parse(client)) < 0) + goto done; + } + + assert(client->state == READING_BODY || client->state == DONE); + +done: + git_buf_dispose(&parser_context.parse_header_name); + git_buf_dispose(&parser_context.parse_header_value); + + return error; +} + +int git_http_client_read_body( + git_http_client *client, + char *buffer, + size_t buffer_size) +{ + http_parser_context parser_context = {0}; + int error = 0; + + if (client->state == DONE) + return 0; + + if (client->state != READING_BODY) { + git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); + return -1; + } + + /* + * Now we'll read from the socket and http_parser will pipeline the + * data directly to the client. + */ + + parser_context.client = client; + parser_context.output_buf = buffer; + parser_context.output_size = buffer_size; + + client->parser.data = &parser_context; + + /* + * Clients expect to get a non-zero amount of data from us. + * With a sufficiently small buffer, one might only read a chunk + * length. Loop until we actually have data to return. + */ + while (!parser_context.output_written) { + error = client_read_and_parse(client); + + if (error <= 0) + goto done; + } + + assert(parser_context.output_written <= INT_MAX); + error = (int)parser_context.output_written; + +done: + if (error < 0) + client->connected = 0; + + return error; +} + +int git_http_client_skip_body(git_http_client *client) +{ + http_parser_context parser_context = {0}; + int error; + + if (client->state == DONE) + return 0; + + if (client->state != READING_BODY) { + git_error_set(GIT_ERROR_HTTP, "client is in invalid state"); + return -1; + } + + parser_context.client = client; + client->parser.data = &parser_context; + + do { + error = client_read_and_parse(client); + + if (parser_context.error != HPE_OK || + (parser_context.parse_status != PARSE_STATUS_OK && + parser_context.parse_status != PARSE_STATUS_NO_OUTPUT)) { + git_error_set(GIT_ERROR_HTTP, + "unexpected data handled in callback"); + error = -1; + } + } while (!error); + + if (error < 0) + client->connected = 0; + + return error; +} + +/* + * Create an http_client capable of communicating with the given remote + * host. + */ +int git_http_client_new( + git_http_client **out, + git_http_client_options *opts) +{ + git_http_client *client; + + assert(out); + + client = git__calloc(1, sizeof(git_http_client)); + GIT_ERROR_CHECK_ALLOC(client); + + git_buf_init(&client->read_buf, GIT_READ_BUFFER_SIZE); + GIT_ERROR_CHECK_ALLOC(client->read_buf.ptr); + + if (opts) + memcpy(&client->opts, opts, sizeof(git_http_client_options)); + + *out = client; + return 0; +} + +GIT_INLINE(void) http_server_close(git_http_server *server) +{ + if (server->stream) { + git_stream_close(server->stream); + git_stream_free(server->stream); + server->stream = NULL; + } + + git_net_url_dispose(&server->url); + + git_vector_free_deep(&server->auth_challenges); + free_auth_context(server); +} + +static void http_client_close(git_http_client *client) +{ + http_server_close(&client->server); + http_server_close(&client->proxy); + + git_buf_dispose(&client->request_msg); + + client->state = 0; + client->request_count = 0; + client->connected = 0; + client->keepalive = 0; +} + +void git_http_client_free(git_http_client *client) +{ + if (!client) + return; + + http_client_close(client); + git_buf_dispose(&client->read_buf); + git__free(client); +} diff --git a/src/transports/httpclient.h b/src/transports/httpclient.h new file mode 100644 index 000000000..da764fd28 --- /dev/null +++ b/src/transports/httpclient.h @@ -0,0 +1,190 @@ +/* + * 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_httpclient_h__ +#define INCLUDE_transports_httpclient_h__ + +#include "common.h" +#include "net.h" + +#define GIT_HTTP_STATUS_CONTINUE 100 +#define GIT_HTTP_STATUS_OK 200 +#define GIT_HTTP_MOVED_PERMANENTLY 301 +#define GIT_HTTP_FOUND 302 +#define GIT_HTTP_SEE_OTHER 303 +#define GIT_HTTP_TEMPORARY_REDIRECT 307 +#define GIT_HTTP_PERMANENT_REDIRECT 308 +#define GIT_HTTP_STATUS_UNAUTHORIZED 401 +#define GIT_HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED 407 + +typedef struct git_http_client git_http_client; + +/** Method for the HTTP request */ +typedef enum { + GIT_HTTP_METHOD_GET, + GIT_HTTP_METHOD_POST, + GIT_HTTP_METHOD_CONNECT +} git_http_method; + +/** An HTTP request */ +typedef struct { + git_http_method method; /**< Method for the request */ + git_net_url *url; /**< Full request URL */ + git_net_url *proxy; /**< Proxy to use */ + + /* Headers */ + const char *accept; /**< Contents of the Accept header */ + const char *content_type; /**< Content-Type header (for POST) */ + git_cred *credentials; /**< Credentials to authenticate with */ + git_cred *proxy_credentials; /**< Credentials for proxy */ + git_strarray *custom_headers; /**< Additional headers to deliver */ + + /* To POST a payload, either set content_length OR set chunked. */ + size_t content_length; /**< Length of the POST body */ + unsigned chunked : 1, /**< Post with chunking */ + expect_continue : 1; /**< Use expect/continue negotiation */ +} git_http_request; + +typedef struct { + int status; + + /* Headers */ + char *content_type; + size_t content_length; + char *location; + + /* Authentication headers */ + unsigned server_auth_schemetypes; /**< Schemes requested by remote */ + unsigned server_auth_credtypes; /**< Supported cred types for remote */ + + unsigned proxy_auth_schemetypes; /**< Schemes requested by proxy */ + unsigned proxy_auth_credtypes; /**< Supported cred types for proxy */ + + unsigned chunked : 1, /**< Response body is chunked */ + resend_credentials : 1; /**< Resend with authentication */ +} git_http_response; + +typedef struct { + /** Certificate check callback for the remote */ + git_transport_certificate_check_cb server_certificate_check_cb; + void *server_certificate_check_payload; + + /** Certificate check callback for the proxy */ + git_transport_certificate_check_cb proxy_certificate_check_cb; + void *proxy_certificate_check_payload; +} git_http_client_options; + +/** + * Create a new httpclient instance with the given options. + * + * @param out pointer to receive the new instance + * @param opts options to create the client with or NULL for defaults + */ +extern int git_http_client_new( + git_http_client **out, + git_http_client_options *opts); + +/* + * Sends a request to the host specified by the request URL. If the + * method is POST, either the the content_length or the chunked flag must + * be specified. The body should be provided in subsequent calls to + * git_http_client_send_body. + * + * @param client the client to write the request to + * @param request the request to send + */ +extern int git_http_client_send_request( + git_http_client *client, + git_http_request *request); + +/* + * After sending a request, there may already be a response to read -- + * either because there was a non-continue response to an expect: continue + * request, or because the server pipelined a response to us before we even + * sent the request. Examine the state. + * + * @param client the client to examine + * @return true if there's already a response to read, false otherwise + */ +extern bool git_http_client_has_response(git_http_client *client); + +/** + * Sends the given buffer to the remote as part of the request body. The + * request must have specified either a content_length or the chunked flag. + * + * @param client the client to write the request body to + * @param buffer the request body + * @param buffer_len number of bytes of the buffer to send + */ +extern int git_http_client_send_body( + git_http_client *client, + const char *buffer, + size_t buffer_len); + +/** + * Reads the headers of a response to a request. This will consume the + * entirety of the headers of a response from the server. The body (if any) + * can be read by calling git_http_client_read_body. Callers must free + * the response with git_http_response_dispose. + * + * @param response pointer to the response object to fill + * @param client the client to read the response from + */ +extern int git_http_client_read_response( + git_http_response *response, + git_http_client *client); + +/** + * Reads some or all of the body of a response. At most buffer_size (or + * INT_MAX) bytes will be read and placed into the buffer provided. The + * number of bytes read will be returned, or 0 to indicate that the end of + * the body has been read. + * + * @param client the client to read the response from + * @param buffer pointer to the buffer to fill + * @param buffer_size the maximum number of bytes to read + * @return the number of bytes read, 0 on end of body, or error code + */ +extern int git_http_client_read_body( + git_http_client *client, + char *buffer, + size_t buffer_size); + +/** + * Reads all of the (remainder of the) body of the response and ignores it. + * None of the data from the body will be returned to the caller. + * + * @param client the client to read the response from + * @return 0 or an error code + */ +extern int git_http_client_skip_body(git_http_client *client); + +/** + * Examines the status code of the response to determine if it is a + * redirect of any type (eg, 301, 302, etc). + * + * @param response the response to inspect + * @return true if the response is a redirect, false otherwise + */ +extern bool git_http_response_is_redirect(git_http_response *response); + +/** + * Frees any memory associated with the response. + * + * @param response the response to free + */ +extern void git_http_response_dispose(git_http_response *response); + +/** + * Frees any memory associated with the client. If any sockets are open, + * they will be closed. + * + * @param client the client to free + */ +extern void git_http_client_free(git_http_client *client); + +#endif diff --git a/src/transports/smart_protocol.c b/src/transports/smart_protocol.c index 87400bb9e..c01656dc4 100644 --- a/src/transports/smart_protocol.c +++ b/src/transports/smart_protocol.c @@ -371,7 +371,7 @@ int git_smart__negotiate_fetch(git_transport *transport, git_repository *repo, c } else if (pkt_type == GIT_PKT_NAK) { continue; } else { - git_error_set(GIT_ERROR_NET, "Unexpected pkt type"); + git_error_set(GIT_ERROR_NET, "unexpected pkt type"); error = -1; goto on_error; } @@ -439,7 +439,7 @@ int git_smart__negotiate_fetch(git_transport *transport, git_repository *repo, c return error; if (pkt_type != GIT_PKT_ACK && pkt_type != GIT_PKT_NAK) { - git_error_set(GIT_ERROR_NET, "Unexpected pkt type"); + git_error_set(GIT_ERROR_NET, "unexpected pkt type"); return -1; } } else { @@ -460,7 +460,7 @@ static int no_sideband(transport_smart *t, struct git_odb_writepack *writepack, do { if (t->cancelled.val) { - git_error_set(GIT_ERROR_NET, "The fetch was cancelled by the user"); + git_error_set(GIT_ERROR_NET, "the fetch was cancelled by the user"); return GIT_EUSER; } @@ -831,7 +831,7 @@ static int parse_report(transport_smart *transport, git_push *push) if (data_pkt_buf.size > 0) { /* If there was data remaining in the pack data buffer, * then the server sent a partial pkt-line */ - git_error_set(GIT_ERROR_NET, "Incomplete pack data pkt-line"); + git_error_set(GIT_ERROR_NET, "incomplete pack data pkt-line"); error = GIT_ERROR; } goto done; diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c index 3a4497da5..e9e53ae86 100644 --- a/src/transports/winhttp.c +++ b/src/transports/winhttp.c @@ -57,6 +57,8 @@ # define DWORD_MAX 0xffffffff #endif +bool git_http__expect_continue = false; + static const char *prefix_https = "https://"; static const char *upload_pack_service = "upload-pack"; static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; @@ -143,7 +145,7 @@ static int apply_userpass_credentials(HINTERNET request, DWORD target, int mecha } else if (mechanisms & GIT_WINHTTP_AUTH_BASIC) { native_scheme = WINHTTP_AUTH_SCHEME_BASIC; } else { - git_error_set(GIT_ERROR_NET, "invalid authentication scheme"); + git_error_set(GIT_ERROR_HTTP, "invalid authentication scheme"); error = -1; goto done; } @@ -182,7 +184,7 @@ static int apply_default_credentials(HINTERNET request, DWORD target, int mechan } else if ((mechanisms & GIT_WINHTTP_AUTH_NTLM) != 0) { native_scheme = WINHTTP_AUTH_SCHEME_NTLM; } else { - git_error_set(GIT_ERROR_NET, "invalid authentication scheme"); + git_error_set(GIT_ERROR_HTTP, "invalid authentication scheme"); return -1; } @@ -285,7 +287,7 @@ static int certificate_check(winhttp_stream *s, int valid) /* If there is no override, we should fail if WinHTTP doesn't think it's fine */ if (t->owner->certificate_check_cb == NULL && !valid) { if (!git_error_last()) - git_error_set(GIT_ERROR_NET, "unknown certificate check failure"); + git_error_set(GIT_ERROR_HTTP, "unknown certificate check failure"); return GIT_ECERTIFICATE; } @@ -309,7 +311,7 @@ static int certificate_check(winhttp_stream *s, int valid) error = valid ? 0 : GIT_ECERTIFICATE; if (error < 0 && !git_error_last()) - git_error_set(GIT_ERROR_NET, "user cancelled certificate check"); + git_error_set(GIT_ERROR_HTTP, "user cancelled certificate check"); return error; } @@ -438,7 +440,7 @@ static int winhttp_stream_connect(winhttp_stream *s) 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); + git_error_set(GIT_ERROR_HTTP, "invalid URL: '%s'", proxy_url); error = -1; goto on_error; } @@ -711,21 +713,21 @@ static void CALLBACK winhttp_status( status = *((DWORD *)info); if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_CN_INVALID)) - git_error_set(GIT_ERROR_NET, "SSL certificate issued for different common name"); + git_error_set(GIT_ERROR_HTTP, "SSL certificate issued for different common name"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_DATE_INVALID)) - git_error_set(GIT_ERROR_NET, "SSL certificate has expired"); + git_error_set(GIT_ERROR_HTTP, "SSL certificate has expired"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CA)) - git_error_set(GIT_ERROR_NET, "SSL certificate signed by unknown CA"); + git_error_set(GIT_ERROR_HTTP, "SSL certificate signed by unknown CA"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CERT)) - git_error_set(GIT_ERROR_NET, "SSL certificate is invalid"); + git_error_set(GIT_ERROR_HTTP, "SSL certificate is invalid"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED)) - git_error_set(GIT_ERROR_NET, "certificate revocation check failed"); + git_error_set(GIT_ERROR_HTTP, "certificate revocation check failed"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REVOKED)) - git_error_set(GIT_ERROR_NET, "SSL certificate was revoked"); + git_error_set(GIT_ERROR_HTTP, "SSL certificate was revoked"); else if ((status & WINHTTP_CALLBACK_STATUS_FLAG_SECURITY_CHANNEL_ERROR)) - git_error_set(GIT_ERROR_NET, "security libraries could not be loaded"); + git_error_set(GIT_ERROR_HTTP, "security libraries could not be loaded"); else - git_error_set(GIT_ERROR_NET, "unknown security error %lu", status); + git_error_set(GIT_ERROR_HTTP, "unknown security error %lu", status); } static int winhttp_connect( @@ -830,7 +832,7 @@ on_error: return error; } -static int do_send_request(winhttp_stream *s, size_t len, int ignore_length) +static int do_send_request(winhttp_stream *s, size_t len, bool chunked) { int attempts; bool success; @@ -841,7 +843,7 @@ static int do_send_request(winhttp_stream *s, size_t len, int ignore_length) } for (attempts = 0; attempts < 5; attempts++) { - if (ignore_length) { + if (chunked) { success = WinHttpSendRequest(s->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, @@ -860,13 +862,13 @@ static int do_send_request(winhttp_stream *s, size_t len, int ignore_length) return success ? 0 : -1; } -static int send_request(winhttp_stream *s, size_t len, int ignore_length) +static int send_request(winhttp_stream *s, size_t len, bool chunked) { int request_failed = 0, cert_valid = 1, error = 0; DWORD ignore_flags; git_error_clear(); - if ((error = do_send_request(s, len, ignore_length)) < 0) { + if ((error = do_send_request(s, len, chunked)) < 0) { if (GetLastError() != ERROR_WINHTTP_SECURE_FAILURE) { git_error_set(GIT_ERROR_OS, "failed to send request"); return -1; @@ -895,7 +897,7 @@ static int send_request(winhttp_stream *s, size_t len, int ignore_length) return -1; } - if ((error = do_send_request(s, len, ignore_length)) < 0) + if ((error = do_send_request(s, len, chunked)) < 0) git_error_set(GIT_ERROR_OS, "failed to send request with unchecked certificate"); return error; @@ -969,7 +971,7 @@ static int winhttp_stream_read( replay: /* Enforce a reasonable cap on the number of replays */ if (replay_count++ >= GIT_HTTP_REPLAY_MAX) { - git_error_set(GIT_ERROR_NET, "too many redirects or authentication replays"); + git_error_set(GIT_ERROR_HTTP, "too many redirects or authentication replays"); return -1; } @@ -984,7 +986,7 @@ replay: if (!s->sent_request) { - if ((error = send_request(s, s->post_body_len, 0)) < 0) + if ((error = send_request(s, s->post_body_len, false)) < 0) return error; s->sent_request = 1; @@ -1128,7 +1130,7 @@ replay: if (!git__prefixcmp_icase(location8, prefix_https)) { /* Upgrade to secure connection; disconnect and start over */ - if (gitno_connection_data_handle_redirect(&t->server.url, location8, s->service_url) < 0) { + if (git_net_url_apply_redirect(&t->server.url, location8, s->service_url) < 0) { git__free(location8); return -1; } @@ -1175,7 +1177,7 @@ replay: } if (HTTP_STATUS_OK != status_code) { - git_error_set(GIT_ERROR_NET, "request failed with status code: %lu", status_code); + git_error_set(GIT_ERROR_HTTP, "request failed with status code: %lu", status_code); return -1; } @@ -1202,7 +1204,7 @@ replay: } if (wcscmp(expected_content_type, content_type)) { - git_error_set(GIT_ERROR_NET, "received unexpected content-type"); + git_error_set(GIT_ERROR_HTTP, "received unexpected content-type"); return -1; } @@ -1237,11 +1239,11 @@ static int winhttp_stream_write_single( /* This implementation of write permits only a single call. */ if (s->sent_request) { - git_error_set(GIT_ERROR_NET, "subtransport configured for only one write"); + git_error_set(GIT_ERROR_HTTP, "subtransport configured for only one write"); return -1; } - if ((error = send_request(s, len, 0)) < 0) + if ((error = send_request(s, len, false)) < 0) return error; s->sent_request = 1; @@ -1268,12 +1270,12 @@ static int put_uuid_string(LPWSTR buffer, size_t buffer_len_cch) if (RPC_S_OK != status && RPC_S_UUID_LOCAL_ONLY != status && RPC_S_UUID_NO_ADDRESS != status) { - git_error_set(GIT_ERROR_NET, "unable to generate name for temp file"); + git_error_set(GIT_ERROR_HTTP, "unable to generate name for temp file"); return -1; } if (buffer_len_cch < UUID_LENGTH_CCH + 1) { - git_error_set(GIT_ERROR_NET, "buffer too small for name of temp file"); + git_error_set(GIT_ERROR_HTTP, "buffer too small for name of temp file"); return -1; } @@ -1380,7 +1382,7 @@ static int winhttp_stream_write_chunked( return -1; } - if ((error = send_request(s, 0, 1)) < 0) + if ((error = send_request(s, 0, true)) < 0) return error; s->sent_request = 1; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 16bad0f6e..6f8a18ec0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,9 +60,11 @@ FUNCTION(ADD_CLAR_TEST name) ENDIF() ENDFUNCTION(ADD_CLAR_TEST) -ADD_CLAR_TEST(offline -v -xonline) -ADD_CLAR_TEST(invasive -v -score::ftruncate -sfilter::stream::bigfile -sodb::largefiles -siterator::workdir::filesystem_gunk -srepo::init -srepo::init::at_filesystem_root) -ADD_CLAR_TEST(online -v -sonline) -ADD_CLAR_TEST(gitdaemon -v -sonline::push) -ADD_CLAR_TEST(ssh -v -sonline::push -sonline::clone::ssh_cert -sonline::clone::ssh_with_paths -sonline::clone::path_whitespace_ssh) -ADD_CLAR_TEST(proxy -v -sonline::clone::proxy) +ADD_CLAR_TEST(offline -v -xonline) +ADD_CLAR_TEST(invasive -v -score::ftruncate -sfilter::stream::bigfile -sodb::largefiles -siterator::workdir::filesystem_gunk -srepo::init -srepo::init::at_filesystem_root) +ADD_CLAR_TEST(online -v -sonline) +ADD_CLAR_TEST(gitdaemon -v -sonline::push) +ADD_CLAR_TEST(ssh -v -sonline::push -sonline::clone::ssh_cert -sonline::clone::ssh_with_paths -sonline::clone::path_whitespace_ssh) +ADD_CLAR_TEST(proxy -v -sonline::clone::proxy) +ADD_CLAR_TEST(auth_clone -v -sonline::clone::cred) +ADD_CLAR_TEST(auth_clone_and_push -v -sonline::clone::push -sonline::push) diff --git a/tests/clar_libgit2_trace.c b/tests/clar_libgit2_trace.c index b6c1c1f53..d4d8d2c37 100644 --- a/tests/clar_libgit2_trace.c +++ b/tests/clar_libgit2_trace.c @@ -1,7 +1,4 @@ #include "clar_libgit2_trace.h" - -#if defined(GIT_TRACE) - #include "clar_libgit2.h" #include "clar_libgit2_timer.h" #include "trace.h" @@ -264,15 +261,3 @@ void cl_global_trace_disable(void) * once. */ } - -#else /* GIT_TRACE */ - -void cl_global_trace_register(void) -{ -} - -void cl_global_trace_disable(void) -{ -} - -#endif /* GIT_TRACE*/ diff --git a/tests/network/joinpath.c b/tests/network/joinpath.c new file mode 100644 index 000000000..da8393b91 --- /dev/null +++ b/tests/network/joinpath.c @@ -0,0 +1,194 @@ +#include "clar_libgit2.h" +#include "net.h" +#include "netops.h" + +static git_net_url source, target; + +void test_network_joinpath__initialize(void) +{ + memset(&source, 0, sizeof(source)); + memset(&target, 0, sizeof(target)); +} + +void test_network_joinpath__cleanup(void) +{ + git_net_url_dispose(&source); + git_net_url_dispose(&target); +} + +void test_network_joinpath__target_paths_and_queries(void) +{ + cl_git_pass(git_net_url_parse(&source, "http://example.com/a/b")); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/c/d")); + cl_assert_equal_s(target.path, "/a/b/c/d"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/c/d?foo")); + cl_assert_equal_s(target.path, "/a/b/c/d"); + cl_assert_equal_s(target.query, "foo"); + git_net_url_dispose(&target); +} + +void test_network_joinpath__source_query_removed(void) +{ + cl_git_pass(git_net_url_parse(&source, "http://example.com/a/b?query&one&two")); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/c/d")); + cl_assert_equal_s(target.path, "/a/b/c/d"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/c/d?foo")); + cl_assert_equal_s(target.path, "/a/b/c/d"); + cl_assert_equal_s(target.query, "foo"); + git_net_url_dispose(&target); +} + +void test_network_joinpath__source_lacks_path(void) +{ + cl_git_pass(git_net_url_parse(&source, "http://example.com")); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/")); + cl_assert_equal_s(target.path, "/"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "")); + cl_assert_equal_s(target.path, "/"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "asdf")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar")); + cl_assert_equal_s(target.path, "/foo/bar"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "asdf?hello")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf?hello")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar?hello")); + cl_assert_equal_s(target.path, "/foo/bar"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); +} + +void test_network_joinpath__source_is_slash(void) +{ + cl_git_pass(git_net_url_parse(&source, "http://example.com/")); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/")); + cl_assert_equal_s(target.path, "/"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "")); + cl_assert_equal_s(target.path, "/"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "asdf")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar")); + cl_assert_equal_s(target.path, "/foo/bar"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "asdf?hello")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf?hello")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar?hello")); + cl_assert_equal_s(target.path, "/foo/bar"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); +} + + +void test_network_joinpath__source_has_query(void) +{ + cl_git_pass(git_net_url_parse(&source, "http://example.com?query")); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/")); + cl_assert_equal_s(target.path, "/"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "")); + cl_assert_equal_s(target.path, "/"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "asdf")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar")); + cl_assert_equal_s(target.path, "/foo/bar"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "asdf?hello")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/asdf?hello")); + cl_assert_equal_s(target.path, "/asdf"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/foo/bar?hello")); + cl_assert_equal_s(target.path, "/foo/bar"); + cl_assert_equal_s(target.query, "hello"); + git_net_url_dispose(&target); +} + + +void test_network_joinpath__empty_query_ignored(void) +{ + cl_git_pass(git_net_url_parse(&source, "http://example.com/foo")); + + cl_git_pass(git_net_url_joinpath(&target, &source, "/bar/baz?")); + cl_assert_equal_s(target.path, "/foo/bar/baz"); + cl_assert_equal_p(target.query, NULL); + git_net_url_dispose(&target); +} diff --git a/tests/network/redirect.c b/tests/network/redirect.c index ce0a080dd..7ce1310db 100644 --- a/tests/network/redirect.c +++ b/tests/network/redirect.c @@ -18,7 +18,7 @@ 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, + cl_git_pass(git_net_url_apply_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"); @@ -32,7 +32,7 @@ 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, + cl_git_pass(git_net_url_apply_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"); @@ -46,7 +46,7 @@ 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, + cl_git_pass(git_net_url_apply_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"); @@ -60,7 +60,7 @@ 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, + cl_git_pass(git_net_url_apply_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"); @@ -73,7 +73,7 @@ void test_network_redirect__redirect_encoded_username_password(void) 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, + cl_git_fail_with(git_net_url_apply_redirect(&conndata, "https://foo.com/bar/baz", NULL), -1); } @@ -81,7 +81,7 @@ void test_network_redirect__redirect_cross_host_denied(void) 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, + cl_git_fail_with(git_net_url_apply_redirect(&conndata, "http://foo.com/bar/baz", NULL), -1); } @@ -89,7 +89,7 @@ void test_network_redirect__redirect_http_downgrade_denied(void) 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, + cl_git_pass(git_net_url_apply_redirect(&conndata, "/zap/baz/biff?bam", NULL)); cl_assert_equal_s(conndata.scheme, "http"); cl_assert_equal_s(conndata.host, "foo.com"); @@ -102,7 +102,7 @@ void test_network_redirect__redirect_relative(void) 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, + cl_git_pass(git_net_url_apply_redirect(&conndata, "/zap/baz/biff?bam", NULL)); cl_assert_equal_s(conndata.scheme, "https"); cl_assert_equal_s(conndata.host, "foo.com"); @@ -115,7 +115,7 @@ void test_network_redirect__redirect_relative_ssl(void) void test_network_redirect__service_query_no_query_params_in_location(void) { cl_git_pass(git_net_url_parse(&conndata, "https://foo.com/bar/info/refs?service=git-upload-pack")); - cl_git_pass(gitno_connection_data_handle_redirect(&conndata, + cl_git_pass(git_net_url_apply_redirect(&conndata, "/baz/info/refs", "/info/refs?service=git-upload-pack")); cl_assert_equal_s(conndata.path, "/baz"); } @@ -123,7 +123,7 @@ void test_network_redirect__service_query_no_query_params_in_location(void) void test_network_redirect__service_query_with_query_params_in_location(void) { cl_git_pass(git_net_url_parse(&conndata, "https://foo.com/bar/info/refs?service=git-upload-pack")); - cl_git_pass(gitno_connection_data_handle_redirect(&conndata, + cl_git_pass(git_net_url_apply_redirect(&conndata, "/baz/info/refs?service=git-upload-pack", "/info/refs?service=git-upload-pack")); cl_assert_equal_s(conndata.path, "/baz"); } diff --git a/tests/online/clone.c b/tests/online/clone.c index cbe0ea798..aed0ab9ce 100644 --- a/tests/online/clone.c +++ b/tests/online/clone.c @@ -30,6 +30,7 @@ static char *_remote_proxy_host = NULL; static char *_remote_proxy_user = NULL; static char *_remote_proxy_pass = NULL; static char *_remote_proxy_selfsigned = NULL; +static char *_remote_expectcontinue = NULL; static int _orig_proxies_need_reset = 0; static char *_orig_http_proxy = NULL; @@ -74,6 +75,10 @@ void test_online_clone__initialize(void) _remote_proxy_user = cl_getenv("GITTEST_REMOTE_PROXY_USER"); _remote_proxy_pass = cl_getenv("GITTEST_REMOTE_PROXY_PASS"); _remote_proxy_selfsigned = cl_getenv("GITTEST_REMOTE_PROXY_SELFSIGNED"); + _remote_expectcontinue = cl_getenv("GITTEST_REMOTE_EXPECTCONTINUE"); + + if (_remote_expectcontinue) + git_libgit2_opts(GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, 1); _orig_proxies_need_reset = 0; } @@ -99,6 +104,7 @@ void test_online_clone__cleanup(void) git__free(_remote_proxy_user); git__free(_remote_proxy_pass); git__free(_remote_proxy_selfsigned); + git__free(_remote_expectcontinue); if (_orig_proxies_need_reset) { cl_setenv("HTTP_PROXY", _orig_http_proxy); @@ -455,8 +461,8 @@ void test_online_clone__can_cancel(void) { g_options.fetch_opts.callbacks.transfer_progress = cancel_at_half; - cl_git_fail_with( - git_clone(&g_repo, LIVE_REPO_URL, "./foo", &g_options), 4321); + cl_git_fail_with(4321, + git_clone(&g_repo, LIVE_REPO_URL, "./foo", &g_options)); } static int cred_cb(git_cred **cred, const char *url, const char *user_from_url, diff --git a/tests/online/push.c b/tests/online/push.c index 592372ba7..8c3150c3d 100644 --- a/tests/online/push.c +++ b/tests/online/push.c @@ -19,6 +19,7 @@ static char *_remote_ssh_pubkey = NULL; static char *_remote_ssh_passphrase = NULL; static char *_remote_default = NULL; +static char *_remote_expectcontinue = NULL; static int cred_acquire_cb(git_cred **, const char *, const char *, unsigned int, void *); @@ -366,12 +367,16 @@ void test_online_push__initialize(void) _remote_ssh_pubkey = cl_getenv("GITTEST_REMOTE_SSH_PUBKEY"); _remote_ssh_passphrase = cl_getenv("GITTEST_REMOTE_SSH_PASSPHRASE"); _remote_default = cl_getenv("GITTEST_REMOTE_DEFAULT"); + _remote_expectcontinue = cl_getenv("GITTEST_REMOTE_EXPECTCONTINUE"); _remote = NULL; /* Skip the test if we're missing the remote URL */ if (!_remote_url) cl_skip(); + if (_remote_expectcontinue) + git_libgit2_opts(GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, 1); + cl_git_pass(git_remote_create(&_remote, _repo, "test", _remote_url)); record_callbacks_data_clear(&_record_cbs_data); @@ -417,10 +422,13 @@ void test_online_push__cleanup(void) git__free(_remote_ssh_pubkey); git__free(_remote_ssh_passphrase); git__free(_remote_default); + git__free(_remote_expectcontinue); /* Freed by cl_git_sandbox_cleanup */ _repo = NULL; + git_libgit2_opts(GIT_OPT_ENABLE_HTTP_EXPECT_CONTINUE, 0); + record_callbacks_data_clear(&_record_cbs_data); cl_fixture_cleanup("testrepo.git"); |