Unverified Commit 4460bf40 by Edward Thomson Committed by GitHub

Merge pull request #5286 from libgit2/ethomson/gssapi

HTTP: Support Apache-based servers with Negotiate
parents 9bcf10e9 e9cef7c4
......@@ -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'
......
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 \
......
......@@ -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"
......
......@@ -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()
......@@ -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
......
......@@ -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;
/**
......
......@@ -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) {
......
......@@ -113,6 +113,7 @@ int git_buf_puts(git_buf *buf, const char *string);
int git_buf_printf(git_buf *buf, const char *format, ...) GIT_FORMAT_PRINTF(2, 3);
int git_buf_vprintf(git_buf *buf, const char *format, va_list ap);
void git_buf_clear(git_buf *buf);
void git_buf_consume_bytes(git_buf *buf, size_t len);
void git_buf_consume(git_buf *buf, const char *end);
void git_buf_truncate(git_buf *buf, size_t len);
void git_buf_shorten(git_buf *buf, size_t amount);
......
......@@ -153,6 +153,187 @@ done:
return error;
}
int git_net_url_joinpath(
git_net_url *out,
git_net_url *one,
const char *two)
{
git_buf path = GIT_BUF_INIT;
const char *query;
size_t one_len, two_len;
git_net_url_dispose(out);
if ((query = strchr(two, '?')) != NULL) {
two_len = query - two;
if (*(++query) != '\0') {
out->query = git__strdup(query);
GIT_ERROR_CHECK_ALLOC(out->query);
}
} else {
two_len = strlen(two);
}
/* Strip all trailing `/`s from the first path */
one_len = one->path ? strlen(one->path) : 0;
while (one_len && one->path[one_len - 1] == '/')
one_len--;
/* Strip all leading `/`s from the second path */
while (*two == '/') {
two++;
two_len--;
}
git_buf_put(&path, one->path, one_len);
git_buf_putc(&path, '/');
git_buf_put(&path, two, two_len);
if (git_buf_oom(&path))
return -1;
out->path = git_buf_detach(&path);
if (one->scheme) {
out->scheme = git__strdup(one->scheme);
GIT_ERROR_CHECK_ALLOC(out->scheme);
}
if (one->host) {
out->host = git__strdup(one->host);
GIT_ERROR_CHECK_ALLOC(out->host);
}
if (one->port) {
out->port = git__strdup(one->port);
GIT_ERROR_CHECK_ALLOC(out->port);
}
if (one->username) {
out->username = git__strdup(one->username);
GIT_ERROR_CHECK_ALLOC(out->username);
}
if (one->password) {
out->password = git__strdup(one->password);
GIT_ERROR_CHECK_ALLOC(out->password);
}
return 0;
}
/*
* Some servers strip the query parameters from the Location header
* when sending a redirect. Others leave it in place.
* Check for both, starting with the stripped case first,
* since it appears to be more common.
*/
static void remove_service_suffix(
git_net_url *url,
const char *service_suffix)
{
const char *service_query = strchr(service_suffix, '?');
size_t full_suffix_len = strlen(service_suffix);
size_t suffix_len = service_query ?
(size_t)(service_query - service_suffix) : full_suffix_len;
size_t path_len = strlen(url->path);
ssize_t truncate = -1;
/*
* Check for a redirect without query parameters,
* like "/newloc/info/refs"'
*/
if (suffix_len && path_len >= suffix_len) {
size_t suffix_offset = path_len - suffix_len;
if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 &&
(!service_query || git__strcmp(url->query, service_query + 1) == 0)) {
truncate = suffix_offset;
}
}
/*
* If we haven't already found where to truncate to remove the
* suffix, check for a redirect with query parameters, like
* "/newloc/info/refs?service=git-upload-pack"
*/
if (truncate < 0 && git__suffixcmp(url->path, service_suffix) == 0)
truncate = path_len - full_suffix_len;
/* Ensure we leave a minimum of '/' as the path */
if (truncate == 0)
truncate++;
if (truncate > 0) {
url->path[truncate] = '\0';
git__free(url->query);
url->query = NULL;
}
}
int git_net_url_apply_redirect(
git_net_url *url,
const char *redirect_location,
const char *service_suffix)
{
git_net_url tmp = GIT_NET_URL_INIT;
int error = 0;
assert(url && redirect_location);
if (redirect_location[0] == '/') {
git__free(url->path);
if ((url->path = git__strdup(redirect_location)) == NULL) {
error = -1;
goto done;
}
} else {
git_net_url *original = url;
if ((error = git_net_url_parse(&tmp, redirect_location)) < 0)
goto done;
/* Validate that this is a legal redirection */
if (original->scheme &&
strcmp(original->scheme, tmp.scheme) != 0 &&
strcmp(tmp.scheme, "https") != 0) {
git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
original->scheme, tmp.scheme);
error = -1;
goto done;
}
if (original->host &&
git__strcasecmp(original->host, tmp.host) != 0) {
git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
original->host, tmp.host);
error = -1;
goto done;
}
git_net_url_swap(url, &tmp);
}
/* Remove the service suffix if it was given to us */
if (service_suffix)
remove_service_suffix(url, service_suffix);
done:
git_net_url_dispose(&tmp);
return error;
}
bool git_net_url_valid(git_net_url *url)
{
return (url->host && url->port && url->path);
}
int git_net_url_is_default_port(git_net_url *url)
{
return (strcmp(url->port, default_port_for_scheme(url->scheme)) == 0);
......@@ -167,6 +348,51 @@ void git_net_url_swap(git_net_url *a, git_net_url *b)
memcpy(b, &tmp, sizeof(git_net_url));
}
int git_net_url_fmt(git_buf *buf, git_net_url *url)
{
git_buf_puts(buf, url->scheme);
git_buf_puts(buf, "://");
if (url->username) {
git_buf_puts(buf, url->username);
if (url->password) {
git_buf_puts(buf, ":");
git_buf_puts(buf, url->password);
}
git_buf_putc(buf, '@');
}
git_buf_puts(buf, url->host);
if (url->port && !git_net_url_is_default_port(url)) {
git_buf_putc(buf, ':');
git_buf_puts(buf, url->port);
}
git_buf_puts(buf, url->path ? url->path : "/");
if (url->query) {
git_buf_putc(buf, '?');
git_buf_puts(buf, url->query);
}
return git_buf_oom(buf) ? -1 : 0;
}
int git_net_url_fmt_path(git_buf *buf, git_net_url *url)
{
git_buf_puts(buf, url->path ? url->path : "/");
if (url->query) {
git_buf_putc(buf, '?');
git_buf_puts(buf, url->query);
}
return git_buf_oom(buf) ? -1 : 0;
}
void git_net_url_dispose(git_net_url *url)
{
if (url->username)
......@@ -179,6 +405,7 @@ void git_net_url_dispose(git_net_url *url)
git__free(url->host); url->host = NULL;
git__free(url->port); url->port = NULL;
git__free(url->path); url->path = NULL;
git__free(url->query); url->query = NULL;
git__free(url->username); url->username = NULL;
git__free(url->password); url->password = NULL;
}
......@@ -22,15 +22,36 @@ typedef struct git_net_url {
#define GIT_NET_URL_INIT { NULL }
/** Parses a string containing a URL into a structure. */
int git_net_url_parse(git_net_url *url, const char *str);
extern int git_net_url_parse(git_net_url *url, const char *str);
/** Appends a path and/or query string to the given URL */
extern int git_net_url_joinpath(
git_net_url *out,
git_net_url *in,
const char *path);
/** Ensures that a URL is minimally valid (contains a host, port and path) */
extern bool git_net_url_valid(git_net_url *url);
/** Returns nonzero if the URL is on the default port. */
int git_net_url_is_default_port(git_net_url *url);
extern int git_net_url_is_default_port(git_net_url *url);
/* Applies a redirect to the URL with a git-aware service suffix. */
extern int git_net_url_apply_redirect(
git_net_url *url,
const char *redirect_location,
const char *service_suffix);
/** Swaps the contents of one URL for another. */
void git_net_url_swap(git_net_url *a, git_net_url *b);
extern void git_net_url_swap(git_net_url *a, git_net_url *b);
/** Places the URL into the given buffer. */
extern int git_net_url_fmt(git_buf *out, git_net_url *url);
/** Place the path and query string into the given buffer. */
extern int git_net_url_fmt_path(git_buf *buf, git_net_url *url);
/** Disposes the contents of the structure. */
void git_net_url_dispose(git_net_url *url);
extern void git_net_url_dispose(git_net_url *url);
#endif
......@@ -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;
}
......@@ -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
......@@ -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;
......
......@@ -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
......
......@@ -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;
......
......@@ -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)
......
......@@ -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"
bool git_http__expect_continue = false;
#define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport)
#define PARSE_ERROR_GENERIC -1
#define PARSE_ERROR_REPLAY -2
/** Look at the user field */
#define PARSE_ERROR_EXT -3
#define CHUNK_SIZE 4096
typedef enum {
HTTP_STATE_NONE = 0,
HTTP_STATE_SENDING_REQUEST,
HTTP_STATE_RECEIVING_RESPONSE,
HTTP_STATE_DONE
} http_state;
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_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 ((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,
http_server *server,
const char *url,
const char *type,
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);
/* Start with URL-specified credentials, if there were any. */
if (!server->url_cred_presented && server->url.username && server->url.password) {
error = apply_url_credentials(&server->cred, server->credtypes, server->url.username, server->url.password);
server->url_cred_presented = 1;
if (error == GIT_PASSTHROUGH) {
/* treat GIT_PASSTHROUGH as if callback isn't set */
error = 1;
}
}
if (error > 0 && callback) {
error = callback(&server->cred, url, server->url.username, server->credtypes, callback_payload);
if (error == GIT_PASSTHROUGH) {
/* treat GIT_PASSTHROUGH as if callback isn't set */
error = 1;
}
}
if (error > 0) {
git_error_set(GIT_ERROR_NET, "%s authentication required but no callback set",
type);
t->parse_error = PARSE_ERROR_GENERIC;
return t->parse_error;
} else if (error < 0) {
t->error = error;
t->parse_error = PARSE_ERROR_EXT;
return t->parse_error;
}
assert(server->cred);
if (!(server->cred->credtype & server->credtypes)) {
git_error_set(GIT_ERROR_NET, "%s credential provider returned an invalid cred type", type);
t->parse_error = PARSE_ERROR_GENERIC;
return t->parse_error;
}
/* Successfully acquired a credential. Start an auth context. */
if (init_auth(server) < 0) {
t->parse_error = PARSE_ERROR_GENERIC;
return t->parse_error;
}
t->parse_error = PARSE_ERROR_REPLAY;
return 0;
}
static void on_auth_success(http_server *server)
{
server->url_cred_presented = 0;
server->authenticated = 1;
}
static int on_headers_complete(http_parser *parser)
{
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;
}
/* 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;
}
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;
}
static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len)
{
parser_context *ctx = (parser_context *) parser->data;
http_subtransport *t = ctx->t;
/* If 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;
}
memcpy(ctx->buffer, str, len);
ctx->buffer += len;
ctx->buf_size -= len;
}
*(ctx->bytes_read) += len;
return 0;
}
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);
git__free(t->content_type);
t->content_type = NULL;
git__free(t->content_length);
t->content_length = NULL;
git__free(t->location);
t->location = NULL;
git_vector_free_deep(&t->proxy.auth_challenges);
git_vector_free_deep(&t->server.auth_challenges);
}
static int write_chunk(git_stream *io, const char *buffer, size_t len)
{
git_buf buf = GIT_BUF_INIT;
/* Chunk header */
git_buf_printf(&buf, "%" PRIxZ "\r\n", len);
if (git_buf_oom(&buf))
return -1;
if (git_stream__write_full(io, buf.ptr, buf.size, 0) < 0) {
git_buf_dispose(&buf);
return -1;
}
git_buf_dispose(&buf);
/* Chunk body */
if (len > 0 && git_stream__write_full(io, buffer, len, 0) < 0)
return -1;
/* Chunk footer */
if (git_stream__write_full(io, "\r\n", 2, 0) < 0)
return -1;
return 0;
}
static int load_proxy_config(http_subtransport *t)
{
int error;
switch (t->owner->proxy.type) {
case GIT_PROXY_NONE:
return 0;
case GIT_PROXY_AUTO:
git__free(t->proxy_url);
t->proxy_url = NULL;
git_proxy_options_init(&t->proxy_opts, GIT_PROXY_OPTIONS_VERSION);
if ((error = git_remote__get_http_proxy(t->owner->owner,
!strcmp(t->server.url.scheme, "https"), &t->proxy_url)) < 0)
return error;
if (!t->proxy_url)
return 0;
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;
case GIT_PROXY_SPECIFIED:
memcpy(&t->proxy_opts, &t->owner->proxy, sizeof(git_proxy_options));
break;
default:
assert(0);
return -1;
}
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 (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);
git_error_state_free(&last_error);
return error;
}
static int stream_connect(
git_stream *stream,
git_net_url *url,
git_transport_certificate_check_cb cert_cb,
void *cb_payload)
{
int error;
GIT_ERROR_CHECK_VERSION(stream, GIT_STREAM_VERSION, "git_stream");
error = git_stream_connect(stream);
if (error && error != GIT_ECERTIFICATE)
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)
GIT_INLINE(void) free_cred(git_cred **cred)
{
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;
/*
* 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 (*cred) {
git_cred_free(*cred);
(*cred) = NULL;
}
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;
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));
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 ((error = gitno_recv(&t->parse_buffer)) < 0) {
goto done;
} else if (error == 0 && t->request_count > 0) {
/* Server closed a keep-alive socket; reconnect. */
auth_replay = true;
goto done;
} else if (error == 0) {
git_error_set(GIT_ERROR_NET, "unexpected disconnection from server");
error = -1;
goto done;
}
/*
* This call to http_parser_execute will invoke the on_*
* 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;
static int handle_auth(
http_server *server,
const char *server_type,
const char *url,
unsigned int allowed_schemetypes,
unsigned int allowed_credtypes,
git_cred_acquire_cb callback,
void *callback_payload)
{
int error = 1;
bytes_parsed = http_parser_execute(&t->parser,
&proxy_parser_settings, t->parse_buffer.data, t->parse_buffer.offset);
if (server->cred)
free_cred(&server->cred);
t->parser.data = NULL;
/* Start with URL-specified credentials, if there were any. */
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;
/* 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;
/* treat GIT_PASSTHROUGH as if callback isn't set */
if (error == GIT_PASSTHROUGH)
error = 1;
}
/* 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;
if (error > 0 && callback) {
error = callback(&server->cred, url, server->url.username, allowed_credtypes, callback_payload);
/* treat GIT_PASSTHROUGH as if callback isn't set */
if (error == GIT_PASSTHROUGH)
error = 1;
}
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));
if (error > 0) {
git_error_set(GIT_ERROR_HTTP, "%s authentication required but no callback set", server_type);
error = -1;
goto done;
}
}
t->request_count++;
if (auth_replay) {
if (t->keepalive && t->parse_finished)
goto replay;
if (!error)
server->auth_schemetypes = allowed_schemetypes;
return PARSE_ERROR_REPLAY;
}
return error;
}
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);
GIT_INLINE(int) handle_remote_auth(
http_stream *stream,
git_http_response *response)
{
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
/*
* 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;
if (response->server_auth_credtypes == 0) {
git_error_set(GIT_ERROR_HTTP, "server requires authentication that we do not support");
return -1;
}
done:
return error;
/* 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 void reset_auth_connection(http_server *server)
GIT_INLINE(int) handle_proxy_auth(
http_stream *stream,
git_http_response *response)
{
/*
* 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;
if (response->proxy_auth_credtypes == 0) {
git_error_set(GIT_ERROR_HTTP, "proxy requires authentication that we do not support");
return -1;
}
/* 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 int http_connect(http_subtransport *t)
static int handle_response(
bool *complete,
http_stream *stream,
git_http_response *response,
bool allow_replay)
{
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);
int error;
auth_replay:
if (t->connected && t->keepalive && t->parse_finished)
return 0;
if ((error = load_proxy_config(t)) < 0)
return error;
*complete = false;
if (t->server.stream) {
git_stream_close(t->server.stream);
git_stream_free(t->server.stream);
t->server.stream = NULL;
if (allow_replay && git_http_response_is_redirect(response)) {
if (!response->location) {
git_error_set(GIT_ERROR_HTTP, "redirect without location");
return -1;
}
if (t->proxy.stream) {
git_stream_close(t->proxy.stream);
git_stream_free(t->proxy.stream);
t->proxy.stream = NULL;
if (git_net_url_apply_redirect(&transport->server.url, response->location, stream->service->url) < 0) {
return -1;
}
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;
return 0;
} else if (git_http_response_is_redirect(response)) {
git_error_set(GIT_ERROR_HTTP, "unexpected redirect");
return -1;
}
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 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;
if ((error = stream_connect(stream, url, cert_cb, cb_payload)) < 0)
goto on_error;
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;
/*
* 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.
*/
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;
}
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;
}
t->proxy.stream = proxy_stream;
t->server.stream = stream;
t->connected = 1;
return 0;
if (response->status != GIT_HTTP_STATUS_OK) {
git_error_set(GIT_ERROR_HTTP, "unexpected http status code: %d", response->status);
return -1;
}
on_error:
if (stream) {
git_stream_close(stream);
git_stream_free(stream);
/* 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;
}
if (proxy_stream) {
git_stream_close(proxy_stream);
git_stream_free(proxy_stream);
/* 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;
}
return error;
*complete = true;
stream->state = HTTP_STATE_RECEIVING_RESPONSE;
return 0;
}
static int http_stream_read(
git_smart_subtransport_stream *stream,
char *buffer,
size_t buf_size,
size_t *bytes_read)
static int lookup_proxy(
bool *out_use,
http_subtransport *transport)
{
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;
const char *proxy;
git_remote *remote;
bool use_ssl;
char *config = NULL;
int error = 0;
replay:
*bytes_read = 0;
auth_replay = false;
*out_use = false;
git_net_url_dispose(&transport->proxy.url);
switch (transport->owner->proxy.type) {
case GIT_PROXY_SPECIFIED:
proxy = transport->owner->proxy.url;
break;
assert(t->connected);
case GIT_PROXY_AUTO:
remote = transport->owner->owner;
use_ssl = !strcmp(transport->server.url.scheme, "https");
if (!s->sent_request) {
git_buf_clear(&request);
clear_parser_state(t);
error = git_remote__get_http_proxy(remote, use_ssl, &config);
if ((error = gen_request(&request, s, 0)) < 0 ||
(error = git_stream__write_full(t->server.stream, request.ptr, request.size, 0)) < 0)
if (error || !config)
goto done;
s->sent_request = 1;
}
proxy = config;
break;
if (!s->received_response) {
if (s->chunked) {
assert(s->verb == post_verb);
default:
return 0;
}
/* Flush, if necessary */
if (s->chunk_buffer_len > 0) {
if ((error = write_chunk(t->server.stream, s->chunk_buffer, s->chunk_buffer_len)) < 0)
if (!proxy ||
(error = git_net_url_parse(&transport->proxy.url, proxy)) < 0)
goto done;
s->chunk_buffer_len = 0;
}
*out_use = true;
/* Write the final chunk. */
if ((error = git_stream__write_full(t->server.stream,
"0\r\n\r\n", 5, 0)) < 0)
goto done;
}
done:
git__free(config);
return error;
}
static int generate_request(
git_net_url *url,
git_http_request *request,
http_stream *stream,
size_t len)
{
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
bool use_proxy = false;
int error;
if ((error = git_net_url_joinpath(url,
&transport->server.url, stream->service->url)) < 0 ||
(error = lookup_proxy(&use_proxy, transport)) < 0)
return error;
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;
s->received_response = 1;
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;
}
while (!*bytes_read && !t->parse_finished) {
size_t data_offset;
return 0;
}
/*
* 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.
/*
* 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.
*/
if (buf_size >= t->parse_buffer.len)
t->parse_buffer.offset = 0;
else
t->parse_buffer.offset = t->parse_buffer.len - buf_size;
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;
data_offset = t->parse_buffer.offset;
*out_len = 0;
if ((error = gitno_recv(&t->parse_buffer)) < 0) {
goto done;
} else if (error == 0 && t->request_count > 0) {
/* Server closed a keep-alive socket; reconnect. */
auth_replay = true;
goto done;
} else if (error == 0) {
git_error_set(GIT_ERROR_NET, "unexpected disconnection from server");
error = -1;
goto done;
if (stream->state == HTTP_STATE_NONE) {
stream->state = HTTP_STATE_SENDING_REQUEST;
stream->replay_count = 0;
}
/*
* 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.
* Formulate the URL, send the request and read the response
* headers. Some of the request body may also be read.
*/
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;
goto done;
} else if (t->parse_error < 0) {
error = -1;
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;
if (complete)
break;
stream->replay_count++;
}
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));
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) {
s->sent_request = 0;
if ((error = http_connect(t)) < 0)
return error;
error = git_http_client_read_body(transport->http_client, buffer, buffer_size);
goto replay;
if (error > 0) {
*out_len = error;
error = 0;
}
done:
git_buf_dispose(&request);
git_net_url_dispose(&url);
git_net_url_dispose(&proxy_url);
git_http_response_dispose(&response);
return error;
}
static int http_stream_write_chunked(
git_smart_subtransport_stream *stream,
const char *buffer,
size_t len)
static bool needs_probe(http_stream *stream)
{
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);
http_subtransport *transport = OWNING_SUBTRANSPORT(stream);
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;
return (transport->server.auth_schemetypes == GIT_HTTP_AUTH_NTLM ||
transport->server.auth_schemetypes == GIT_HTTP_AUTH_NEGOTIATE);
}
/* 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;
static int send_probe(http_stream *stream)
{
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;
s->chunk_buffer_len = 0;
/* NTLM requires a full challenge/response */
if (transport->server.auth_schemetypes == GIT_HTTP_AUTH_NTLM)
steps = GIT_AUTH_STEPS_NTLM;
if (len > 0) {
memcpy(s->chunk_buffer, buffer, len);
s->chunk_buffer_len = len;
}
}
/*
* 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.
*/
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;
}
return 0;
done:
git_http_response_dispose(&response);
git_net_url_dispose(&url);
return error;
}
static int http_stream_write_single(
git_smart_subtransport_stream *stream,
/*
* 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 = GIT_CONTAINER_OF(stream, http_stream, parent);
http_subtransport *t = OWNING_SUBTRANSPORT(s);
git_buf request = GIT_BUF_INIT;
assert(t->connected);
if (s->sent_request) {
git_error_set(GIT_ERROR_NET, "subtransport configured for only one write");
return -1;
}
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;
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;
return 0;
while (stream->state == HTTP_STATE_NONE &&
stream->replay_count < GIT_HTTP_REPLAY_MAX) {
on_error:
git_buf_dispose(&request);
return -1;
}
git_net_url_dispose(&url);
git_http_response_dispose(&response);
static void http_stream_free(git_smart_subtransport_stream *stream)
{
http_stream *s = GIT_CONTAINER_OF(stream, http_stream, parent);
/*
* 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 (needs_probe(stream) && (error = send_probe(stream)) < 0)
goto done;
if (s->chunk_buffer)
git__free(s->chunk_buffer);
/* 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;
if (s->redirect_url)
git__free(s->redirect_url);
if (request.expect_continue &&
git_http_client_has_response(transport->http_client)) {
bool complete;
git__free(s);
}
/*
* 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;
}
static int http_stream_alloc(http_subtransport *t,
git_smart_subtransport_stream **stream)
{
http_stream *s;
stream->replay_count++;
}
if (!stream)
return -1;
if (stream->state == HTTP_STATE_NONE) {
git_error_set(GIT_ERROR_HTTP,
"too many redirects or authentication replays");
error = -1;
goto done;
}
s = git__calloc(sizeof(http_stream), 1);
GIT_ERROR_CHECK_ALLOC(s);
assert(stream->state == HTTP_STATE_SENDING_REQUEST);
s->parent.subtransport = &t->parent;
s->parent.read = http_stream_read;
s->parent.write = http_stream_write_single;
s->parent.free = http_stream_free;
error = git_http_client_send_body(transport->http_client, buffer, len);
*stream = (git_smart_subtransport_stream *)s;
return 0;
done:
git_http_response_dispose(&response);
git_net_url_dispose(&url);
return error;
}
static int http_uploadpack_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 = upload_pack_service;
s->service_url = upload_pack_ls_service_url;
s->verb = get_verb;
/*
* 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;
return 0;
}
*out_len = 0;
static int http_uploadpack(
http_subtransport *t,
git_smart_subtransport_stream **stream)
{
http_stream *s;
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 (http_stream_alloc(t, stream) < 0)
return -1;
assert(complete);
stream->state = HTTP_STATE_RECEIVING_RESPONSE;
}
s = (http_stream *)*stream;
error = git_http_client_read_body(client, buffer, buffer_size);
s->service = upload_pack_service;
s->service_url = upload_pack_service_url;
s->verb = post_verb;
if (error > 0) {
*out_len = error;
error = 0;
}
return 0;
done:
git_http_response_dispose(&response);
return error;
}
static int http_receivepack_ls(
http_subtransport *t,
git_smart_subtransport_stream **stream)
static void http_stream_free(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;
http_stream *s = GIT_CONTAINER_OF(stream, http_stream, parent);
git__free(s);
}
static int http_receivepack(
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;
/* 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 ((ret = http_connect(t)) < 0)
return ret;
if (!git_net_url_valid(&transport->server.url) &&
(error = git_net_url_parse(&transport->server.url, url)) < 0)
return error;
switch (action) {
case GIT_SERVICE_UPLOADPACK_LS:
return http_uploadpack_ls(t, stream);
if ((service = select_service(action)) == NULL) {
git_error_set(GIT_ERROR_HTTP, "invalid action");
return -1;
}
case GIT_SERVICE_UPLOADPACK:
return http_uploadpack(t, stream);
stream = git__calloc(sizeof(http_stream), 1);
GIT_ERROR_CHECK_ALLOC(stream);
case GIT_SERVICE_RECEIVEPACK_LS:
return http_receivepack_ls(t, stream);
if (!transport->http_client) {
git_http_client_options opts = {0};
case GIT_SERVICE_RECEIVEPACK:
return http_receivepack(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;
*stream = NULL;
if (git_http_client_new(&transport->http_client, &opts) < 0)
return -1;
}
static int http_close(git_smart_subtransport *subtransport)
{
http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent);
clear_parser_state(t);
}
t->connected = 0;
stream->service = service;
stream->parent.subtransport = &transport->parent;
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);
git_http_client_free(transport->http_client);
http_close(subtransport);
git__free(t);
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;
}
......
......@@ -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();
......
/*
* 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);
}
/*
* 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
......@@ -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;
......
......@@ -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;
......
......@@ -66,3 +66,5 @@ 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)
#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*/
#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);
}
......@@ -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");
}
......@@ -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,
......
......@@ -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");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment