summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2019-12-12 13:25:32 +1000
committerEdward Thomson <ethomson@edwardthomson.com>2020-01-24 10:16:36 -0600
commiteacecebdcf12e72f692319eb0332f2e270ed359a (patch)
tree45a5680ffb1bfa9af301ea2f565c94d32062a098
parenta591f3623c755328c12afe08dce9acfb19d0252d (diff)
downloadlibgit2-eacecebdcf12e72f692319eb0332f2e270ed359a.tar.gz
httpclient: introduce a simple http implementation
Introduce a new http client implementation that can GET and POST to remote URLs. Consumers can use `git_http_client_init` to create a new client, `git_http_client_send_request` to send a request to the remote server and `git_http_client_read_response` to read the response. The http client implementation will perform the I/O with the remote server (http or https) but does not understand the git smart transfer protocol. This allows us to split the concerns of the http subtransport from the actual http implementation.
-rw-r--r--src/transports/httpclient.c898
-rw-r--r--src/transports/httpclient.h147
2 files changed, 1045 insertions, 0 deletions
diff --git a/src/transports/httpclient.c b/src/transports/httpclient.c
new file mode 100644
index 000000000..5d432b3b3
--- /dev/null
+++ b/src/transports/httpclient.c
@@ -0,0 +1,898 @@
+/*
+ * 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 "net.h"
+#include "stream.h"
+#include "streams/socket.h"
+#include "streams/tls.h"
+#include "auth.h"
+
+#define GIT_READ_BUFFER_SIZE 8192
+
+typedef struct {
+ git_net_url url;
+ git_stream *stream;
+} git_http_server;
+
+typedef enum {
+ NONE = 0,
+ SENDING_BODY,
+ SENT_REQUEST,
+ 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_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;
+
+ http_client_state state;
+
+ http_parser parser;
+
+ git_http_server server;
+ git_http_server proxy;
+
+ unsigned request_count;
+ unsigned connected : 1,
+ keepalive : 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;
+};
+
+bool git_http_response_is_redirect(git_http_response *response)
+{
+ return (response->status == 301 ||
+ response->status == 302 ||
+ response->status == 303 ||
+ response->status == 307 ||
+ response->status == 308);
+}
+
+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_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_NET,
+ "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_NET,
+ "multiple content-length headers");
+ return -1;
+ }
+
+ if (git__strntol64(&len, value->ptr, value->size,
+ NULL, 10) < 0 || len < 0) {
+ git_error_set(GIT_ERROR_NET,
+ "invalid content-length");
+ return -1;
+ }
+
+ response->content_length = (size_t)len;
+ } else if (!strcasecmp("Location", name->ptr)) {
+ if (response->location) {
+ git_error_set(GIT_ERROR_NET,
+ "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_NET,
+ "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_NET,
+ "header value seen at unexpected time");
+ return ctx->parse_status = PARSE_STATUS_ERROR;
+ }
+
+ 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_NET,
+ "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);
+
+ /* Stop parsing. */
+ http_parser_pause(parser, 1);
+
+ ctx->client->state = READING_BODY;
+ 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;
+
+ 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);
+}
+
+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";
+ }
+
+ return NULL;
+}
+
+static int generate_request(
+ git_http_client *client,
+ git_http_request *request)
+{
+ const char *method, *path, *sep, *query;
+ git_buf *buf;
+ size_t i;
+
+ assert(client && request);
+
+ git_buf_clear(&client->request_msg);
+ buf = &client->request_msg;
+
+ method = name_for_method(request->method);
+ path = request->url->path ? request->url->path : "/";
+ sep = request->url->query ? "?" : "";
+ query = request->url->query ? request->url->query : "";
+
+ git_buf_printf(buf, "%s %s%s%s HTTP/1.1\r\n",
+ method, path, sep, query);
+
+ 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 (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_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;
+}
+
+/*
+ * 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 int http_client_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)
+ client->connected = 0;
+
+ return 0;
+}
+
+static void reset_parser(git_http_client *client)
+{
+ git_buf_clear(&client->read_buf);
+}
+
+static int http_client_connect(git_http_client *client)
+{
+ git_net_url *url;
+ git_stream *proxy_stream = NULL, *stream = NULL;
+ git_transport_certificate_check_cb cert_cb;
+ void *cb_payload;
+ int error;
+
+ if (client->connected && client->keepalive &&
+ (client->state == NONE || client->state == DONE))
+ return 0;
+
+ git_trace(GIT_TRACE_DEBUG, "Connecting to %s:%s",
+ client->server.url.host, client->server.url.port);
+
+ if (client->server.stream) {
+ git_stream_close(client->server.stream);
+ git_stream_free(client->server.stream);
+ client->server.stream = NULL;
+ }
+
+ if (client->proxy.stream) {
+ git_stream_close(client->proxy.stream);
+ git_stream_free(client->proxy.stream);
+ client->proxy.stream = NULL;
+ }
+
+ reset_parser(client);
+
+ client->connected = 0;
+ client->keepalive = 0;
+ client->request_count = 0;
+
+ if (client->proxy.url.host) {
+ url = &client->proxy.url;
+ cert_cb = client->opts.proxy_certificate_check_cb;
+ cb_payload = client->opts.proxy_certificate_check_payload;
+ } else {
+ url = &client->server.url;
+ cert_cb = client->opts.server_certificate_check_cb;
+ cb_payload = client->opts.server_certificate_check_payload;
+ }
+
+ if (strcasecmp(url->scheme, "https") == 0) {
+ error = git_tls_stream_new(&stream, url->host, url->port);
+ } else if (strcasecmp(url->scheme, "http") == 0) {
+ error = git_socket_stream_new(&stream, url->host, url->port);
+ } else {
+ git_error_set(GIT_ERROR_NET, "unknown http scheme '%s'",
+ url->scheme);
+ error = -1;
+ }
+
+ if (error < 0)
+ goto on_error;
+
+ if ((error = stream_connect(stream, url, cert_cb, cb_payload)) < 0)
+ goto on_error;
+
+ client->proxy.stream = proxy_stream;
+ client->server.stream = stream;
+ client->connected = 1;
+ return 0;
+
+on_error:
+ if (stream) {
+ git_stream_close(stream);
+ git_stream_free(stream);
+ }
+
+ if (proxy_stream) {
+ git_stream_close(proxy_stream);
+ git_stream_free(proxy_stream);
+ }
+
+ return error;
+}
+
+int git_http_client_send_request(
+ git_http_client *client,
+ git_http_request *request)
+{
+ int error = -1;
+
+ assert(client && request);
+
+ http_parser_init(&client->parser, HTTP_RESPONSE);
+ git_buf_clear(&client->read_buf);
+
+ 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_setup_hosts(client, request)) < 0 ||
+ (error = http_client_connect(client)) < 0 ||
+ (error = generate_request(client, request)) < 0 ||
+ (error = stream_write(&client->server,
+ client->request_msg.ptr,
+ client->request_msg.size)) < 0)
+ 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;
+ } else {
+ client->state = SENT_REQUEST;
+ }
+
+done:
+ return error;
+}
+
+int git_http_client_send_body(
+ git_http_client *client,
+ const char *buffer,
+ size_t buffer_len)
+{
+ int error;
+
+ assert(client && client->state == SENDING_BODY);
+ assert(buffer_len <= client->request_body_remain);
+
+ error = stream_write(&client->server, buffer, buffer_len);
+
+ if (error == 0)
+ client->request_body_remain -= buffer_len;
+
+ return error;
+}
+
+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(git_http_client *client)
+{
+ char *buf = client->read_buf.ptr + client->read_buf.size;
+ size_t max_len;
+ ssize_t read_len;
+
+ /*
+ * 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_NET, "no room in output buffer");
+ return -1;
+ }
+
+ read_len = git_stream_read(client->server.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;
+}
+
+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_NET, "unexpectedly large parse");
+ return -1;
+ }
+
+ if (parser->upgrade) {
+ git_error_set(GIT_ERROR_NET, "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_NET, "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_NET,
+ "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_NET, "unexpected EOF");
+ return -1;
+ }
+
+ git_buf_consume_bytes(&client->read_buf, parsed_len);
+
+ return (int)parsed_len;
+}
+
+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 (client->request_body_len && client->request_body_remain) {
+ git_error_set(GIT_ERROR_NET, "truncated write");
+ return -1;
+ }
+
+ client->state = SENT_REQUEST;
+ } else if (client->state != SENT_REQUEST) {
+ git_error_set(GIT_ERROR_NET, "client is in invalid state");
+ return -1;
+ }
+
+ client->state = READING_RESPONSE;
+ 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_NET, "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;
+}
+
+/*
+ * 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);
+}
+
+static void http_client_close(git_http_client *client)
+{
+ http_server_close(&client->server);
+ http_server_close(&client->proxy);
+
+ git_buf_dispose(&client->request_msg);
+
+ client->state = 0;
+ client->request_count = 0;
+ client->connected = 0;
+ client->keepalive = 0;
+}
+
+void git_http_client_free(git_http_client *client)
+{
+ if (!client)
+ return;
+
+ http_client_close(client);
+ git_buf_dispose(&client->read_buf);
+ git__free(client);
+}
diff --git a/src/transports/httpclient.h b/src/transports/httpclient.h
new file mode 100644
index 000000000..73ae356c4
--- /dev/null
+++ b/src/transports/httpclient.h
@@ -0,0 +1,147 @@
+/*
+ * 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"
+
+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;
+
+/** 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_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;
+} 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);
+
+/**
+ * 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);
+
+/**
+ * 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