/* * 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 "httpclient.h" #include "http.h" #include "auth.h" #include "auth_negotiate.h" #include "auth_ntlm.h" #include "git2/sys/credential.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_CREDENTIAL_DEFAULT, git_http_auth_negotiate }, { GIT_HTTP_AUTH_NTLM, "NTLM", GIT_CREDENTIAL_USERPASS_PLAINTEXT, git_http_auth_ntlm }, { GIT_HTTP_AUTH_BASIC, "Basic", GIT_CREDENTIAL_USERPASS_PLAINTEXT, git_http_auth_basic }, }; /* * Use a 16kb read buffer to match the maximum size of a TLS packet. This * is critical for compatibility with SecureTransport, which will always do * a network read on every call, even if it has data buffered to return to * you. That buffered data may be the _end_ of a keep-alive response, so * if SecureTransport performs another network read, it will wait until the * server ultimately times out before it returns that buffered data to you. * Since SecureTransport only reads a single TLS packet at a time, by * calling it with a read buffer that is the maximum size of a TLS packet, * we ensure that it will never buffer. */ #define GIT_READ_BUFFER_SIZE (16 * 1024) 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_str parse_header_name; git_str 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_str request_msg; git_str 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) { if (!response) return; 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_str *name = &ctx->parse_header_name; git_str *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_str_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_str_clear(&ctx->parse_header_name); git_str_clear(&ctx->parse_header_value); /* Fall through */ case PARSE_HEADER_NONE: case PARSE_HEADER_NAME: ctx->parse_header_state = PARSE_HEADER_NAME; if (git_str_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_str_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; } GIT_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); } static 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_credential *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_credential *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_str *buf, git_http_server *server, const char *header_name, git_credential *credentials) { git_http_auth_context *auth = server->auth_context; git_vector *challenges = &server->auth_challenges; const char *challenge; git_str token = GIT_STR_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 challenge"); error = GIT_EAUTH; goto done; } if (token.size > 0) error = git_str_printf(buf, "%s: %s\r\n", header_name, token.ptr); done: git_str_dispose(&token); return error; } GIT_INLINE(int) apply_server_credentials( git_str *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_str *buf, git_http_client *client, git_http_request *request) { return apply_credentials(buf, &client->proxy, "Proxy-Authorization", request->proxy_credentials); } static int puts_host_and_port(git_str *buf, git_net_url *url, bool force_port) { bool ipv6 = git_net_url_is_ipv6(url); if (ipv6) git_str_putc(buf, '['); git_str_puts(buf, url->host); if (ipv6) git_str_putc(buf, ']'); if (force_port || !git_net_url_is_default_port(url)) { git_str_putc(buf, ':'); git_str_puts(buf, url->port); } return git_str_oom(buf) ? -1 : 0; } static int generate_connect_request( git_http_client *client, git_http_request *request) { git_str *buf; int error; git_str_clear(&client->request_msg); buf = &client->request_msg; git_str_puts(buf, "CONNECT "); puts_host_and_port(buf, &client->server.url, true); git_str_puts(buf, " HTTP/1.1\r\n"); git_str_puts(buf, "User-Agent: "); git_http__user_agent(buf); git_str_puts(buf, "\r\n"); git_str_puts(buf, "Host: "); puts_host_and_port(buf, &client->server.url, true); git_str_puts(buf, "\r\n"); if ((error = apply_proxy_credentials(buf, client, request) < 0)) return -1; git_str_puts(buf, "\r\n"); return git_str_oom(buf) ? -1 : 0; } static bool use_connect_proxy(git_http_client *client) { return client->proxy.url.host && !strcmp(client->server.url.scheme, "https"); } static int generate_request( git_http_client *client, git_http_request *request) { git_str *buf; size_t i; int error; GIT_ASSERT_ARG(client); GIT_ASSERT_ARG(request); git_str_clear(&client->request_msg); buf = &client->request_msg; /* GET|POST path HTTP/1.1 */ git_str_puts(buf, name_for_method(request->method)); git_str_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_str_puts(buf, " HTTP/1.1\r\n"); git_str_puts(buf, "User-Agent: "); git_http__user_agent(buf); git_str_puts(buf, "\r\n"); git_str_puts(buf, "Host: "); puts_host_and_port(buf, request->url, false); git_str_puts(buf, "\r\n"); if (request->accept) git_str_printf(buf, "Accept: %s\r\n", request->accept); else git_str_puts(buf, "Accept: */*\r\n"); if (request->content_type) git_str_printf(buf, "Content-Type: %s\r\n", request->content_type); if (request->chunked) git_str_puts(buf, "Transfer-Encoding: chunked\r\n"); if (request->content_length > 0) git_str_printf(buf, "Content-Length: %"PRIuZ "\r\n", request->content_length); if (request->expect_continue) git_str_printf(buf, "Expect: 100-continue\r\n"); if ((error = apply_server_credentials(buf, client, request)) < 0 || (!use_connect_proxy(client) && (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_str_printf(buf, "%s\r\n", hdr); } } git_str_puts(buf, "\r\n"); if (git_str_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; GIT_ASSERT_ARG(client); GIT_ASSERT_ARG(request); GIT_ASSERT(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 port %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; GIT_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 = use_connect_proxy(client); 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 port %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) { http_parser_context *parser_context = client->parser.data; 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_str 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 (parser_context->output_size) max_len = min(max_len, parser_context->output_size); 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 (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 */ GIT_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_str_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; goto done; } 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; } done: git_str_clear(&client->read_buf); } int git_http_client_send_request( git_http_client *client, git_http_request *request) { git_http_response response = {0}; int error = -1; GIT_ASSERT_ARG(client); GIT_ASSERT_ARG(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_str url = GIT_STR_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 : ""); git_str_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_str hdr = GIT_STR_INIT; int error; GIT_ASSERT_ARG(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) { GIT_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_str_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_str_dispose(&hdr); return error; } static int complete_request(git_http_client *client) { int error = 0; GIT_ASSERT_ARG(client); GIT_ASSERT(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; GIT_ASSERT_ARG(response); GIT_ASSERT_ARG(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); if (client->current_server == PROXY) { git_vector_free_deep(&client->proxy.auth_challenges); } else if(client->current_server == SERVER) { git_vector_free_deep(&client->server.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; } GIT_ASSERT(client->state == READING_BODY || client->state == DONE); done: git_str_dispose(&parser_context.parse_header_name); git_str_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, * so we either block until we have data to return, until we * hit EOF or there's an error. Do this in a loop, since we * may end up reading only some stream metadata (like chunk * information). */ while (!parser_context.output_written) { error = client_read_and_parse(client); if (error <= 0) goto done; if (client->state == DONE) break; } GIT_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 >= 0 && client->state != 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; GIT_ASSERT_ARG(out); client = git__calloc(1, sizeof(git_http_client)); GIT_ERROR_CHECK_ALLOC(client); git_str_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; } /* Update the options of an existing httpclient instance. */ void git_http_client_set_options( git_http_client *client, git_http_client_options *opts) { if (opts) memcpy(&client->opts, opts, sizeof(git_http_client_options)); } 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_str_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_str_dispose(&client->read_buf); git__free(client); }