summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2020-01-24 11:08:44 -0600
committerGitHub <noreply@github.com>2020-01-24 11:08:44 -0600
commit4460bf40c9e935acb853b5d61279a50014ede0b3 (patch)
treeb847c846e79c815c329ea6e7b843bdbf6ef82c86
parent9bcf10e97e9b7c8fc12961e3950ce0a8e64231ce (diff)
parente9cef7c4b16b2cb572ac19fcd39217f7934cfa99 (diff)
downloadlibgit2-4460bf40c9e935acb853b5d61279a50014ede0b3.tar.gz
Merge pull request #5286 from libgit2/ethomson/gssapi
HTTP: Support Apache-based servers with Negotiate
-rw-r--r--azure-pipelines.yml17
-rw-r--r--azure-pipelines/docker/xenial4
-rwxr-xr-xazure-pipelines/test.sh74
-rw-r--r--cmake/Modules/SelectGSSAPI.cmake2
-rw-r--r--include/git2/common.h8
-rw-r--r--include/git2/errors.h3
-rw-r--r--src/buffer.c5
-rw-r--r--src/buffer.h1
-rw-r--r--src/net.c227
-rw-r--r--src/net.h29
-rw-r--r--src/netops.c98
-rw-r--r--src/netops.h11
-rw-r--r--src/settings.c5
-rw-r--r--src/trace.h2
-rw-r--r--src/transports/auth_negotiate.c59
-rw-r--r--src/transports/auth_ntlm.h3
-rw-r--r--src/transports/http.c1828
-rw-r--r--src/transports/http.h3
-rw-r--r--src/transports/httpclient.c1526
-rw-r--r--src/transports/httpclient.h190
-rw-r--r--src/transports/smart_protocol.c8
-rw-r--r--src/transports/winhttp.c58
-rw-r--r--tests/CMakeLists.txt14
-rw-r--r--tests/clar_libgit2_trace.c15
-rw-r--r--tests/network/joinpath.c194
-rw-r--r--tests/network/redirect.c20
-rw-r--r--tests/online/clone.c10
-rw-r--r--tests/online/push.c8
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);
diff --git a/src/net.c b/src/net.c
index a1dc408e0..d42fce52d 100644
--- a/src/net.c
+++ b/src/net.c
@@ -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;
}
diff --git a/src/net.h b/src/net.h
index 6df129089..7e72db13f 100644
--- a/src/net.h
+++ b/src/net.h
@@ -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");