diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2019-12-22 14:12:24 -0800 |
---|---|---|
committer | Edward Thomson <ethomson@edwardthomson.com> | 2020-01-24 10:39:51 -0600 |
commit | b9c5b15a7958ab4ecb83504a6d858efd18610297 (patch) | |
tree | f3cb851fdf271c298dadc648324e8979fc1fbbca /src | |
parent | bf55facf15e5b6897ff7305162a77d92089f6c82 (diff) | |
download | libgit2-b9c5b15a7958ab4ecb83504a6d858efd18610297.tar.gz |
http: use the new httpclient
Untangle the notion of the http transport from the actual http
implementation. The http transport now uses the httpclient.
Diffstat (limited to 'src')
-rw-r--r-- | src/transports/http.c | 1780 | ||||
-rw-r--r-- | src/transports/http.h | 1 |
2 files changed, 407 insertions, 1374 deletions
diff --git a/src/transports/http.c b/src/transports/http.c index 9e35f23c2..c535f2a35 100644 --- a/src/transports/http.c +++ b/src/transports/http.c @@ -22,406 +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" bool git_http__expect_continue = false; -git_http_auth_scheme auth_schemes[] = { - { GIT_HTTP_AUTH_NEGOTIATE, "Negotiate", GIT_CREDTYPE_DEFAULT, git_http_auth_negotiate }, - { GIT_HTTP_AUTH_NTLM, "NTLM", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_ntlm }, - { GIT_HTTP_AUTH_BASIC, "Basic", GIT_CREDTYPE_USERPASS_PLAINTEXT, git_http_auth_basic }, -}; - -static const char *upload_pack_service = "upload-pack"; -static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; -static const char *upload_pack_service_url = "/git-upload-pack"; -static const char *receive_pack_service = "receive-pack"; -static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack"; -static const char *receive_pack_service_url = "/git-receive-pack"; -static const char *get_verb = "GET"; -static const char *post_verb = "POST"; - -#define AUTH_HEADER_SERVER "Authorization" -#define AUTH_HEADER_PROXY "Proxy-Authorization" - -#define SERVER_TYPE_REMOTE "remote" -#define SERVER_TYPE_PROXY "proxy" - -#define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport) +typedef enum { + HTTP_STATE_NONE = 0, + HTTP_STATE_SENDING_REQUEST, + HTTP_STATE_RECEIVING_RESPONSE, + HTTP_STATE_DONE +} http_state; -#define PARSE_ERROR_GENERIC -1 -#define PARSE_ERROR_REPLAY -2 -/** Look at the user field */ -#define PARSE_ERROR_EXT -3 - -#define CHUNK_SIZE 4096 - -enum last_cb { - NONE, - FIELD, - VALUE -}; +typedef struct { + git_http_method method; + const char *url; + const char *request_type; + const char *response_type; + unsigned chunked : 1; +} http_service; typedef struct { git_smart_subtransport_stream parent; - const char *service; - const char *service_url; - char *redirect_url; - const char *verb; - char *chunk_buffer; - unsigned chunk_buffer_len; - unsigned sent_request : 1, - received_response : 1, - chunked : 1; + const http_service *service; + http_state state; + unsigned replay_count; } http_stream; typedef struct { git_net_url url; - git_stream *stream; - - git_http_auth_t authtypes; - git_credtype_t credtypes; git_cred *cred; - unsigned url_cred_presented : 1, - authenticated : 1; - git_http_authtype_t prior_authtype; - - 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, @@ -438,1096 +114,466 @@ static int apply_url_credentials( return GIT_PASSTHROUGH; } -static int init_auth(http_server *server) +GIT_INLINE(void) free_cred(git_cred **cred) { - git_http_auth_scheme *s, *scheme = NULL; - char *c, *challenge = NULL; - size_t i; - int error; - - git_vector_foreach(&server->auth_challenges, i, c) { - s = scheme_for_challenge(c, server->cred); - - if (s && !!(s->credtypes & server->credtypes)) { - scheme = s; - challenge = c; - break; - } - } - - if (!scheme) { - git_error_set(GIT_ERROR_NET, "no authentication mechanism could be negotiated"); - return -1; + if (*cred) { + git_cred_free(*cred); + (*cred) = NULL; } - - if ((error = scheme->init_context(&server->auth_context, &server->url)) == GIT_PASSTHROUGH) - return 0; - else if (error < 0) - return error; - - if (server->auth_context->set_challenge && - (error = server->auth_context->set_challenge(server->auth_context, challenge)) < 0) - return error; - - return 0; } -static int on_auth_required( - http_parser *parser, +static int handle_auth( http_server *server, + const char *server_type, const char *url, - const char *type, + unsigned int allowed_schemetypes, + unsigned int allowed_credtypes, git_cred_acquire_cb callback, void *callback_payload) { - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; int error = 1; - if (parse_authenticate_response(server) < 0) { - t->parse_error = PARSE_ERROR_GENERIC; - return t->parse_error; - } - - /* If we're in the middle of challenge/response auth, continue */ - if (parser->status_code == 407 || parser->status_code == 401) { - if (server->auth_context && !auth_context_complete(server)) { - t->parse_error = PARSE_ERROR_REPLAY; - return 0; - } - } - - /* Enforce a reasonable cap on the number of replays */ - if (t->replay_count++ >= GIT_HTTP_REPLAY_MAX) { - git_error_set(GIT_ERROR_NET, "too many redirects or authentication replays"); - return t->parse_error = PARSE_ERROR_GENERIC; - } - - if (!server->credtypes) { - git_error_set(GIT_ERROR_NET, "%s requested authentication but did not negotiate mechanisms", type); - t->parse_error = PARSE_ERROR_GENERIC; - return t->parse_error; - } - - free_auth_context(server); - free_cred(&server->cred); + if (server->cred) + free_cred(&server->cred); /* Start with URL-specified credentials, if there were any. */ - if (!server->url_cred_presented && server->url.username && server->url.password) { - error = apply_url_credentials(&server->cred, server->credtypes, server->url.username, server->url.password); + if ((allowed_credtypes & GIT_CREDTYPE_USERPASS_PLAINTEXT) && + !server->url_cred_presented && + server->url.username && + server->url.password) { + error = apply_url_credentials(&server->cred, allowed_credtypes, server->url.username, server->url.password); server->url_cred_presented = 1; - if (error == GIT_PASSTHROUGH) { - /* treat GIT_PASSTHROUGH as if callback isn't set */ + /* treat GIT_PASSTHROUGH as if callback isn't set */ + if (error == GIT_PASSTHROUGH) error = 1; - } } if (error > 0 && callback) { - error = callback(&server->cred, url, server->url.username, server->credtypes, callback_payload); + error = callback(&server->cred, url, server->url.username, allowed_credtypes, callback_payload); - if (error == GIT_PASSTHROUGH) { - /* treat GIT_PASSTHROUGH as if callback isn't set */ + /* treat GIT_PASSTHROUGH as if callback isn't set */ + if (error == GIT_PASSTHROUGH) error = 1; - } } if (error > 0) { - git_error_set(GIT_ERROR_NET, "%s authentication required but no callback set", - type); - t->parse_error = PARSE_ERROR_GENERIC; - return t->parse_error; - } else if (error < 0) { - t->error = error; - t->parse_error = PARSE_ERROR_EXT; - return t->parse_error; - } - - assert(server->cred); - - if (!(server->cred->credtype & server->credtypes)) { - git_error_set(GIT_ERROR_NET, "%s credential provider returned an invalid cred type", type); - t->parse_error = PARSE_ERROR_GENERIC; - return t->parse_error; - } - - /* Successfully acquired a credential. Start an auth context. */ - if (init_auth(server) < 0) { - t->parse_error = PARSE_ERROR_GENERIC; - return t->parse_error; + git_error_set(GIT_ERROR_NET, "%s authentication required but no callback set", server_type); + error = -1; } - t->parse_error = PARSE_ERROR_REPLAY; - return 0; -} + if (!error) + server->auth_schemetypes = allowed_schemetypes; -static void on_auth_success(http_server *server) -{ - server->url_cred_presented = 0; - server->authenticated = 1; + return error; } -static int on_headers_complete(http_parser *parser) +GIT_INLINE(int) handle_remote_auth( + http_stream *stream, + git_http_response *response) { - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - http_stream *s = ctx->s; - git_buf buf = GIT_BUF_INIT; - - /* Both parse_header_name and parse_header_value are populated - * and ready for consumption. */ - if (t->last_cb == VALUE && on_header_ready(t) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; - - /* Check for a proxy authentication failure. */ - if (parser->status_code == 407 && get_verb == s->verb) - return on_auth_required( - parser, - &t->proxy, - t->proxy_opts.url, - SERVER_TYPE_PROXY, - t->proxy_opts.credentials, - t->proxy_opts.payload); - else - on_auth_success(&t->proxy); - - /* Check for an authentication failure. */ - if (parser->status_code == 401 && get_verb == s->verb) - return on_auth_required( - parser, - &t->server, - t->owner->url, - SERVER_TYPE_REMOTE, - t->owner->cred_acquire_cb, - t->owner->cred_acquire_payload); - else - on_auth_success(&t->server); - - /* Check for a redirect. - * Right now we only permit a redirect to the same hostname. */ - if ((parser->status_code == 301 || - parser->status_code == 302 || - (parser->status_code == 303 && get_verb == s->verb) || - parser->status_code == 307 || - parser->status_code == 308) && - t->location) { - - if (git_net_url_apply_redirect(&t->server.url, t->location, s->service_url) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; - - 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; - } + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); - /* The Content-Type header must match our expectation. */ - if (get_verb == s->verb) - git_buf_printf(&buf, - "application/x-git-%s-advertisement", - ctx->s->service); - else - git_buf_printf(&buf, - "application/x-git-%s-result", - ctx->s->service); - - if (git_buf_oom(&buf)) - return t->parse_error = PARSE_ERROR_GENERIC; - - if (strcmp(t->content_type, git_buf_cstr(&buf))) { - git_buf_dispose(&buf); - git_error_set(GIT_ERROR_NET, - "invalid Content-Type: %s", - t->content_type); - return t->parse_error = PARSE_ERROR_GENERIC; + if (response->server_auth_credtypes == 0) { + git_error_set(GIT_ERROR_NET, "server requires authentication that we do not support"); + return -1; } - git_buf_dispose(&buf); - - return 0; -} - -static int on_message_complete(http_parser *parser) -{ - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - - t->parse_finished = 1; - t->keepalive = http_should_keep_alive(parser); - - return 0; + /* Otherwise, prompt for credentials. */ + return handle_auth( + &transport->server, + SERVER_TYPE_REMOTE, + transport->owner->url, + response->server_auth_schemetypes, + response->server_auth_credtypes, + transport->owner->cred_acquire_cb, + transport->owner->cred_acquire_payload); } -static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len) +GIT_INLINE(int) handle_proxy_auth( + http_stream *stream, + git_http_response *response) { - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - - /* If there's no buffer set, we're explicitly ignoring the body. */ - if (ctx->buffer) { - if (ctx->buf_size < len) { - git_error_set(GIT_ERROR_NET, "can't fit data in the buffer"); - return t->parse_error = PARSE_ERROR_GENERIC; - } + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); - memcpy(ctx->buffer, str, len); - ctx->buffer += len; - ctx->buf_size -= len; + if (response->proxy_auth_credtypes == 0) { + git_error_set(GIT_ERROR_NET, "proxy requires authentication that we do not support"); + return -1; } - *(ctx->bytes_read) += len; - - return 0; + /* Otherwise, prompt for credentials. */ + return handle_auth( + &transport->proxy, + SERVER_TYPE_PROXY, + transport->owner->proxy.url, + response->server_auth_schemetypes, + response->proxy_auth_credtypes, + transport->owner->proxy.credentials, + transport->owner->proxy.payload); } -static void clear_parser_state(http_subtransport *t) -{ - http_parser_init(&t->parser, HTTP_RESPONSE); - gitno_buffer_setup_fromstream(t->server.stream, - &t->parse_buffer, - t->parse_buffer_data, - sizeof(t->parse_buffer_data)); - - t->last_cb = NONE; - t->parse_error = 0; - t->parse_finished = 0; - t->keepalive = 0; - - git_buf_dispose(&t->parse_header_name); - git_buf_init(&t->parse_header_name, 0); - - git_buf_dispose(&t->parse_header_value); - git_buf_init(&t->parse_header_value, 0); - - 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) +static int handle_response( + bool *complete, + http_stream *stream, + git_http_response *response, + bool allow_replay) { - git_buf buf = GIT_BUF_INIT; - - /* Chunk header */ - git_buf_printf(&buf, "%" PRIxZ "\r\n", len); - - if (git_buf_oom(&buf)) - return -1; + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + int error; - if (git_stream__write_full(io, buf.ptr, buf.size, 0) < 0) { - git_buf_dispose(&buf); - return -1; - } + *complete = false; - git_buf_dispose(&buf); + if (allow_replay && git_http_response_is_redirect(response)) { + if (!response->location) { + git_error_set(GIT_ERROR_NET, "redirect without location"); + return -1; + } - /* Chunk body */ - if (len > 0 && git_stream__write_full(io, buffer, len, 0) < 0) - return -1; + if (git_net_url_apply_redirect(&transport->server.url, response->location, stream->service->url) < 0) { + return -1; + } - /* Chunk footer */ - if (git_stream__write_full(io, "\r\n", 2, 0) < 0) + return 0; + } else if (git_http_response_is_redirect(response)) { + git_error_set(GIT_ERROR_NET, "unexpected redirect"); return -1; + } - return 0; -} - -static int load_proxy_config(http_subtransport *t) -{ - int error; - - switch (t->owner->proxy.type) { - case GIT_PROXY_NONE: + /* If we're in the middle of challenge/response auth, continue. */ + if (allow_replay && response->resend_credentials) { 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) + } else if (allow_replay && response->status == 401) { + if ((error = handle_remote_auth(stream, response)) < 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; + return git_http_client_skip_body(transport->http_client); + } else if (allow_replay && response->status == 407) { + if ((error = handle_proxy_auth(stream, response)) < 0) + return error; - default: - assert(0); + return git_http_client_skip_body(transport->http_client); + } else if (response->status == 401 || response->status == 407) { + git_error_set(GIT_ERROR_NET, "unexpected authentication failure"); 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) + if (response->status != 200) { + git_error_set(GIT_ERROR_NET, "unexpected http status code: %d", response->status); return -1; - - git_buf_puts(buf, "\r\n"); - - return git_buf_oom(buf) ? -1 : 0; -} - -static int proxy_headers_complete(http_parser *parser) -{ - parser_context *ctx = (parser_context *) parser->data; - http_subtransport *t = ctx->t; - - /* Both parse_header_name and parse_header_value are populated - * and ready for consumption. */ - if (t->last_cb == VALUE && on_header_ready(t) < 0) - return t->parse_error = PARSE_ERROR_GENERIC; - - /* - * 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; + /* The response must contain a Content-Type header. */ + if (!response->content_type) { + git_error_set(GIT_ERROR_NET, "no content-type header in response"); + return -1; } - /* 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; + /* The Content-Type header must match our expectation. */ + if (strcmp(response->content_type, stream->service->response_type) != 0) { + git_error_set(GIT_ERROR_NET, "invalid content-type: '%s'", response->content_type); + return -1; } - if (!t->content_length || strcmp(t->content_length, "0") == 0) - t->parse_finished = 1; - + *complete = true; + stream->state = HTTP_STATE_RECEIVING_RESPONSE; return 0; } -static int proxy_connect( - git_stream **out, git_stream *proxy_stream, http_subtransport *t) +static int lookup_proxy( + bool *out_use, + http_subtransport *transport) { - 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); + const char *proxy; + git_remote *remote; + bool use_ssl; + char *config = NULL; + int error = 0; - while (!bytes_read && !t->parse_finished) { - t->parse_buffer.offset = 0; + *out_use = false; + git_net_url_dispose(&transport->proxy.url); - 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; - } + switch (transport->owner->proxy.type) { + case GIT_PROXY_SPECIFIED: + proxy = transport->owner->proxy.url; + break; - /* - * This call to http_parser_execute will invoke the on_* - * callbacks. Since we don't care about the body of the response, - * we can set our buffer to NULL. - */ - ctx.t = t; - ctx.s = NULL; - ctx.buffer = NULL; - ctx.buf_size = 0; - ctx.bytes_read = &bytes_read; - - /* Set the context, call the parser, then unset the context. */ - t->parser.data = &ctx; - - bytes_parsed = http_parser_execute(&t->parser, - &proxy_parser_settings, t->parse_buffer.data, t->parse_buffer.offset); - - t->parser.data = NULL; - - /* Ensure that we didn't get a redirect; unsupported. */ - if (t->location) { - git_error_set(GIT_ERROR_NET, "proxy server sent unsupported redirect during CONNECT"); - error = -1; - goto done; - } + case GIT_PROXY_AUTO: + remote = transport->owner->owner; + use_ssl = !strcmp(transport->server.url.scheme, "https"); - /* 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; - } + error = git_remote__get_http_proxy(remote, use_ssl, &config); - if (bytes_parsed != t->parse_buffer.offset) { - git_error_set(GIT_ERROR_NET, - "HTTP parser error: %s", - http_errno_description((enum http_errno)t->parser.http_errno)); - error = -1; + if (error || !config) goto done; - } - } - - t->request_count++; - if (auth_replay) { - if (t->keepalive && t->parse_finished) - goto replay; + proxy = config; + break; - return PARSE_ERROR_REPLAY; + default: + return 0; } - if ((error = git_tls_stream_wrap(out, proxy_stream, t->server.url.host)) == 0) - error = stream_connect(*out, &t->server.url, - t->owner->certificate_check_cb, - t->owner->message_cb_payload); + if (!proxy || + (error = git_net_url_parse(&transport->proxy.url, proxy)) < 0) + goto done; - /* - * 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; + *out_use = true; done: + git__free(config); return error; } -static void reset_auth_connection(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->authenticated && - server->auth_context && - server->auth_context->connection_affinity) { - server->prior_authtype = server->auth_context->type; - - free_auth_context(server); - - server->url_cred_presented = 0; - server->authenticated = 0; - } -} - -static int http_connect(http_subtransport *t) +static int generate_request( + git_net_url *url, + git_http_request *request, + http_stream *stream, + size_t len) { - 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); + bool use_proxy = false; int error; -auth_replay: - if (t->connected && t->keepalive && t->parse_finished) - return 0; - - if ((error = load_proxy_config(t)) < 0) + if ((error = git_net_url_joinpath(url, + &transport->server.url, stream->service->url)) < 0 || + (error = lookup_proxy(&use_proxy, transport)) < 0) return error; - if (t->server.stream) { - git_stream_close(t->server.stream); - git_stream_free(t->server.stream); - t->server.stream = NULL; - } - - if (t->proxy.stream) { - git_stream_close(t->proxy.stream); - git_stream_free(t->proxy.stream); - t->proxy.stream = NULL; - } - - reset_auth_connection(&t->server); - reset_auth_connection(&t->proxy); - - t->connected = 0; - t->keepalive = 0; - t->request_count = 0; + 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; - if (t->proxy_opts.type == GIT_PROXY_SPECIFIED) { - url = &t->proxy.url; - cert_cb = t->proxy_opts.certificate_check; - cb_payload = t->proxy_opts.payload; - } else { - url = &t->server.url; - cert_cb = t->owner->certificate_check_cb; - cb_payload = t->owner->message_cb_payload; + if (stream->service->method == GIT_HTTP_METHOD_POST) { + request->chunked = stream->service->chunked; + request->content_length = stream->service->chunked ? 0 : len; + request->content_type = stream->service->request_type; + request->accept = stream->service->response_type; + request->expect_continue = git_http__expect_continue; } - if (strcmp(url->scheme, "https") == 0) - error = git_tls_stream_new(&stream, url->host, url->port); - else - error = git_socket_stream_new(&stream, url->host, url->port); - - if (error < 0) - goto on_error; - - if ((error = stream_connect(stream, url, cert_cb, cb_payload)) < 0) - goto on_error; - - /* - * 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; - } - } - - t->proxy.stream = proxy_stream; - t->server.stream = stream; - t->connected = 1; return 0; - -on_error: - if (stream) { - git_stream_close(stream); - git_stream_free(stream); - } - - if (proxy_stream) { - git_stream_close(proxy_stream); - git_stream_free(proxy_stream); - } - - return error; } +/* + * Read from an HTTP transport - for the first invocation of this function + * (ie, when stream->state == HTTP_STATE_NONE), we'll send a GET request + * to the remote host. We will stream that data back on all subsequent + * calls. + */ static int http_stream_read( - git_smart_subtransport_stream *stream, + git_smart_subtransport_stream *s, char *buffer, - size_t buf_size, - size_t *bytes_read) -{ - http_stream *s = (http_stream *)stream; - http_subtransport *t = OWNING_SUBTRANSPORT(s); - parser_context ctx; - size_t bytes_parsed; - git_buf request = GIT_BUF_INIT; - bool auth_replay; - int error = 0; - -replay: - *bytes_read = 0; - auth_replay = false; - - assert(t->connected); - - if (!s->sent_request) { - git_buf_clear(&request); - clear_parser_state(t); - - if ((error = gen_request(&request, s, 0)) < 0 || - (error = git_stream__write_full(t->server.stream, request.ptr, request.size, 0)) < 0) - goto done; - - s->sent_request = 1; - } - - if (!s->received_response) { - if (s->chunked) { - assert(s->verb == post_verb); - - /* Flush, if necessary */ - if (s->chunk_buffer_len > 0) { - if ((error = write_chunk(t->server.stream, s->chunk_buffer, s->chunk_buffer_len)) < 0) - goto done; - - s->chunk_buffer_len = 0; - } + 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; - /* Write the final chunk. */ - if ((error = git_stream__write_full(t->server.stream, - "0\r\n\r\n", 5, 0)) < 0) - goto done; - } + *out_len = 0; - s->received_response = 1; + if (stream->state == HTTP_STATE_NONE) { + stream->state = HTTP_STATE_SENDING_REQUEST; + stream->replay_count = 0; } - while (!*bytes_read && !t->parse_finished) { - size_t data_offset; - - /* - * Make the parse_buffer think it's as full of data as - * the buffer, so it won't try to recv more data than - * we can put into it. - * - * data_offset is the actual data offset from which we - * should tell the parser to start reading. - */ - if (buf_size >= t->parse_buffer.len) - t->parse_buffer.offset = 0; - else - t->parse_buffer.offset = t->parse_buffer.len - buf_size; - - data_offset = t->parse_buffer.offset; - - if ((error = gitno_recv(&t->parse_buffer)) < 0) { - goto done; - } else if (error == 0 && t->request_count > 0) { - /* Server closed a keep-alive socket; reconnect. */ - auth_replay = true; - goto done; - } else if (error == 0) { - git_error_set(GIT_ERROR_NET, "unexpected disconnection from server"); - error = -1; + /* + * Formulate the URL, send the request and read the response + * headers. Some of the request body may also be read. + */ + while (stream->state == HTTP_STATE_SENDING_REQUEST && + stream->replay_count < GIT_HTTP_REPLAY_MAX) { + git_net_url_dispose(&url); + git_net_url_dispose(&proxy_url); + git_http_response_dispose(&response); + + if ((error = generate_request(&url, &request, stream, 0)) < 0 || + (error = git_http_client_send_request( + transport->http_client, &request)) < 0 || + (error = git_http_client_read_response( + &response, transport->http_client)) < 0 || + (error = handle_response(&complete, stream, &response, true)) < 0) goto done; - } - /* - * This call to http_parser_execute will result in invocations - * of the on_* family of callbacks, including on_body_fill_buffer - * which will write into the target buffer. Set up the buffer - * for it to write into _unless_ we got an auth failure; in - * that case we only care about the headers and don't need to - * bother copying the body. - */ - ctx.t = t; - ctx.s = s; - ctx.buffer = auth_replay ? NULL : buffer; - ctx.buf_size = auth_replay ? 0 : buf_size; - ctx.bytes_read = bytes_read; - - /* Set the context, call the parser, then unset the context. */ - t->parser.data = &ctx; - - bytes_parsed = http_parser_execute(&t->parser, - &t->settings, - t->parse_buffer.data + data_offset, - t->parse_buffer.offset - data_offset); - - t->parser.data = NULL; - - /* On a 401, read the rest of the response then retry. */ - if (t->parse_error == PARSE_ERROR_REPLAY) { - auth_replay = true; - } else if (t->parse_error == PARSE_ERROR_EXT) { - error = t->error; - goto done; - } else if (t->parse_error < 0) { - error = -1; - goto done; - } + if (complete) + break; - if (bytes_parsed != t->parse_buffer.offset - data_offset) { - git_error_set(GIT_ERROR_NET, - "HTTP parser error: %s", - http_errno_description((enum http_errno)t->parser.http_errno)); - error = -1; - goto done; - } + stream->replay_count++; } - t->request_count++; + if (stream->state == HTTP_STATE_SENDING_REQUEST) { + git_error_set(GIT_ERROR_NET, "too many redirects or authentication replays"); + error = -1; + goto done; + } - if (auth_replay) { - s->sent_request = 0; + assert (stream->state == HTTP_STATE_RECEIVING_RESPONSE); - 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, +/* +* 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); - - assert(t->connected); + 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; - /* Send the request, if necessary */ - if (!s->sent_request) { - git_buf request = GIT_BUF_INIT; + while (stream->state == HTTP_STATE_NONE && + stream->replay_count < GIT_HTTP_REPLAY_MAX) { - clear_parser_state(t); + git_net_url_dispose(&url); + git_http_response_dispose(&response); - if (gen_request(&request, s, 0) < 0) - return -1; + if ((error = generate_request(&url, &request, stream, len)) < 0 || + (error = git_http_client_send_request( + transport->http_client, &request)) < 0) + goto done; - if (git_stream__write_full(t->server.stream, request.ptr, - request.size, 0) < 0) { - git_buf_dispose(&request); - return -1; + if (request.expect_continue && + git_http_client_has_response(transport->http_client)) { + bool complete; + + /* + * If we got a response to an expect/continue, then + * it's something other than a 100 and we should + * deal with the response somehow. + */ + if ((error = git_http_client_read_response(&response, transport->http_client)) < 0 || + (error = handle_response(&complete, stream, &response, true)) < 0) + goto done; + } else { + stream->state = HTTP_STATE_SENDING_REQUEST; } - git_buf_dispose(&request); - - s->sent_request = 1; + stream->replay_count++; } - 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; + if (stream->state == HTTP_STATE_NONE) { + git_error_set(GIT_ERROR_NET, + "too many redirects or authentication replays"); + error = -1; + goto done; } - else { - /* Append as much to the buffer as we can */ - int count = min(CHUNK_SIZE - s->chunk_buffer_len, len); - if (!s->chunk_buffer) { - s->chunk_buffer = git__malloc(CHUNK_SIZE); - GIT_ERROR_CHECK_ALLOC(s->chunk_buffer); - } - - memcpy(s->chunk_buffer + s->chunk_buffer_len, buffer, count); - s->chunk_buffer_len += count; - buffer += count; - len -= count; - - /* 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; - - s->chunk_buffer_len = 0; + assert(stream->state == HTTP_STATE_SENDING_REQUEST); - if (len > 0) { - memcpy(s->chunk_buffer, buffer, len); - s->chunk_buffer_len = len; - } - } - } + error = git_http_client_send_body(transport->http_client, buffer, len); - 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, - const char *buffer, - size_t len) +/* +* 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 *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); + http_stream *stream = (http_stream *)s; + http_subtransport *transport = OWNING_SUBTRANSPORT(stream); + git_http_response response = {0}; + bool complete; + int error; - if (gen_request(&request, s, len) < 0) - return -1; + *out_len = 0; - if (git_stream__write_full(t->server.stream, request.ptr, request.size, 0) < 0) - goto on_error; + if (stream->state == HTTP_STATE_SENDING_REQUEST) { + if ((error = git_http_client_read_response(&response, transport->http_client)) < 0 || + (error = handle_response(&complete, stream, &response, false)) < 0) + goto done; - if (len && git_stream__write_full(t->server.stream, buffer, len, 0) < 0) - goto on_error; + assert(complete); + stream->state = HTTP_STATE_RECEIVING_RESPONSE; + } - git_buf_dispose(&request); - s->sent_request = 1; + error = git_http_client_read_body(transport->http_client, buffer, buffer_size); - return 0; + if (error > 0) { + *out_len = error; + error = 0; + } -on_error: - git_buf_dispose(&request); - return -1; +done: + git_http_response_dispose(&response); + return error; } static void http_stream_free(git_smart_subtransport_stream *stream) { http_stream *s = GIT_CONTAINER_OF(stream, http_stream, parent); - - if (s->chunk_buffer) - git__free(s->chunk_buffer); - git__free(s); } -static int http_stream_alloc(http_subtransport *t, - git_smart_subtransport_stream **stream) -{ - http_stream *s; - - if (!stream) - return -1; - - s = git__calloc(sizeof(http_stream), 1); - GIT_ERROR_CHECK_ALLOC(s); - - s->parent.subtransport = &t->parent; - s->parent.read = http_stream_read; - s->parent.write = http_stream_write_single; - s->parent.free = http_stream_free; - - *stream = (git_smart_subtransport_stream *)s; - return 0; -} - -static int http_uploadpack_ls( - http_subtransport *t, - git_smart_subtransport_stream **stream) +static const http_service *select_service(git_smart_service_t action) { - http_stream *s; - - if (http_stream_alloc(t, stream) < 0) - return -1; - - s = (http_stream *)*stream; - - s->service = upload_pack_service; - s->service_url = upload_pack_ls_service_url; - s->verb = get_verb; - - return 0; -} - -static int http_uploadpack( - http_subtransport *t, - git_smart_subtransport_stream **stream) -{ - http_stream *s; - - if (http_stream_alloc(t, stream) < 0) - return -1; - - s = (http_stream *)*stream; - - s->service = upload_pack_service; - s->service_url = upload_pack_service_url; - s->verb = post_verb; - - return 0; -} - -static int http_receivepack_ls( - http_subtransport *t, - git_smart_subtransport_stream **stream) -{ - http_stream *s; - - if (http_stream_alloc(t, stream) < 0) - return -1; - - s = (http_stream *)*stream; - - s->service = receive_pack_service; - s->service_url = receive_pack_ls_service_url; - s->verb = get_verb; - - return 0; -} - -static int http_receivepack( - http_subtransport *t, - git_smart_subtransport_stream **stream) -{ - http_stream *s; - - if (http_stream_alloc(t, stream) < 0) - return -1; - - s = (http_stream *)*stream; - - /* Use Transfer-Encoding: chunked for this request */ - s->chunked = 1; - s->parent.write = http_stream_write_chunked; - - s->service = receive_pack_service; - s->service_url = receive_pack_service_url; - s->verb = post_verb; + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return &upload_pack_ls_service; + case GIT_SERVICE_UPLOADPACK: + return &upload_pack_service; + case GIT_SERVICE_RECEIVEPACK_LS: + return &receive_pack_ls_service; + case GIT_SERVICE_RECEIVEPACK: + return &receive_pack_service; + } - return 0; + return NULL; } static int http_action( - git_smart_subtransport_stream **stream, - git_smart_subtransport *subtransport, + git_smart_subtransport_stream **out, + git_smart_subtransport *t, const char *url, git_smart_service_t action) { - http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent); - int ret; + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); + http_stream *stream; + const http_service *service; + int error; + + assert(out && t); - assert(stream); + *out = NULL; /* * If we've seen a redirect then preserve the location that we've @@ -1536,103 +582,89 @@ static int http_action( * have redirected us from HTTP->HTTPS and is using an auth mechanism * that would be insecure in plaintext (eg, HTTP Basic). */ - if ((!t->server.url.host || !t->server.url.port || !t->server.url.path) && - (ret = git_net_url_parse(&t->server.url, url)) < 0) - return ret; - - assert(t->server.url.host && t->server.url.port && t->server.url.path); + if (!git_net_url_valid(&transport->server.url) && + (error = git_net_url_parse(&transport->server.url, url)) < 0) + return error; - if ((ret = http_connect(t)) < 0) - return ret; + if ((service = select_service(action)) == NULL) { + git_error_set(GIT_ERROR_NET, "invalid action"); + return -1; + } - switch (action) { - case GIT_SERVICE_UPLOADPACK_LS: - return http_uploadpack_ls(t, stream); + stream = git__calloc(sizeof(http_stream), 1); + GIT_ERROR_CHECK_ALLOC(stream); - case GIT_SERVICE_UPLOADPACK: - return http_uploadpack(t, stream); + if (!transport->http_client) { + git_http_client_options opts = {0}; - case GIT_SERVICE_RECEIVEPACK_LS: - return http_receivepack_ls(t, stream); + opts.server_certificate_check_cb = transport->owner->certificate_check_cb; + opts.server_certificate_check_payload = transport->owner->message_cb_payload; + opts.proxy_certificate_check_cb = transport->owner->proxy.certificate_check; + opts.proxy_certificate_check_payload = transport->owner->proxy.payload; - case GIT_SERVICE_RECEIVEPACK: - return http_receivepack(t, stream); + if (git_http_client_new(&transport->http_client, &opts) < 0) + return -1; } - *stream = NULL; - return -1; -} - -static int http_close(git_smart_subtransport *subtransport) -{ - http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent); - - clear_parser_state(t); + stream->service = service; + stream->parent.subtransport = &transport->parent; - t->connected = 0; - - if (t->server.stream) { - git_stream_close(t->server.stream); - git_stream_free(t->server.stream); - t->server.stream = NULL; + if (service->method == GIT_HTTP_METHOD_GET) { + stream->parent.read = http_stream_read; + } else { + stream->parent.write = http_stream_write; + stream->parent.read = http_stream_read_response; } - if (t->proxy.stream) { - git_stream_close(t->proxy.stream); - git_stream_free(t->proxy.stream); - t->proxy.stream = NULL; - } + stream->parent.free = http_stream_free; - free_cred(&t->server.cred); - free_cred(&t->proxy.cred); + *out = (git_smart_subtransport_stream *)stream; + return 0; +} - free_auth_context(&t->server); - free_auth_context(&t->proxy); +static int http_close(git_smart_subtransport *t) +{ + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); - t->server.url_cred_presented = false; - t->proxy.url_cred_presented = false; + free_cred(&transport->server.cred); + free_cred(&transport->proxy.cred); - git_net_url_dispose(&t->server.url); - git_net_url_dispose(&t->proxy.url); + transport->server.url_cred_presented = false; + transport->proxy.url_cred_presented = false; - git__free(t->proxy_url); - t->proxy_url = NULL; + git_net_url_dispose(&transport->server.url); + git_net_url_dispose(&transport->proxy.url); return 0; } -static void http_free(git_smart_subtransport *subtransport) +static void http_free(git_smart_subtransport *t) { - http_subtransport *t = GIT_CONTAINER_OF(subtransport, http_subtransport, parent); + http_subtransport *transport = GIT_CONTAINER_OF(t, http_subtransport, parent); - http_close(subtransport); - git__free(t); + git_http_client_free(transport->http_client); + + http_close(t); + git__free(transport); } int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner, void *param) { - http_subtransport *t; + http_subtransport *transport; GIT_UNUSED(param); - if (!out) - return -1; - - t = git__calloc(sizeof(http_subtransport), 1); - GIT_ERROR_CHECK_ALLOC(t); + assert(out); - t->owner = (transport_smart *)owner; - t->parent.action = http_action; - t->parent.close = http_close; - t->parent.free = http_free; + transport = git__calloc(sizeof(http_subtransport), 1); + GIT_ERROR_CHECK_ALLOC(transport); - t->settings.on_header_field = on_header_field; - t->settings.on_header_value = on_header_value; - t->settings.on_headers_complete = on_headers_complete; - t->settings.on_body = on_body_fill_buffer; - t->settings.on_message_complete = on_message_complete; + transport->owner = (transport_smart *)owner; + transport->parent.action = http_action; + transport->parent.close = http_close; + transport->parent.free = http_free; - *out = (git_smart_subtransport *) t; + *out = (git_smart_subtransport *) transport; return 0; } diff --git a/src/transports/http.h b/src/transports/http.h index 6f698c9dd..c02109cec 100644 --- a/src/transports/http.h +++ b/src/transports/http.h @@ -9,6 +9,7 @@ #define INCLUDE_transports_http_h__ #include "buffer.h" +#include "httpclient.h" #define GIT_HTTP_REPLAY_MAX 15 |