diff options
author | Edward Thomson <ethomson@edwardthomson.com> | 2018-10-28 21:27:56 +0000 |
---|---|---|
committer | Edward Thomson <ethomson@edwardthomson.com> | 2018-11-28 15:46:57 +0000 |
commit | 5d4e1e040f6c4530ac18f3ce82685e780192b2bb (patch) | |
tree | 0e2f8ebf93bc178be27959ba295e6196562020db | |
parent | 43b592ac84dbd3d649022ff9503f00ecc83d5278 (diff) | |
download | libgit2-5d4e1e040f6c4530ac18f3ce82685e780192b2bb.tar.gz |
http: use CONNECT to talk to proxies
Natively support HTTPS connections through proxies by speaking CONNECT
to the proxy and then adding a TLS connection on top of the socket.
-rw-r--r-- | src/transports/http.c | 237 |
1 files changed, 224 insertions, 13 deletions
diff --git a/src/transports/http.c b/src/transports/http.c index 77e1f1be1..6d2aaf9cf 100644 --- a/src/transports/http.c +++ b/src/transports/http.c @@ -68,8 +68,7 @@ typedef struct { unsigned chunk_buffer_len; unsigned sent_request : 1, received_response : 1, - chunked : 1, - replay_count : 3; + chunked : 1; } http_stream; typedef struct { @@ -103,11 +102,13 @@ typedef struct { 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 parse_finished : 1; + unsigned parse_finished : 1, + replay_count : 3; } http_subtransport; typedef struct { @@ -260,8 +261,11 @@ static int gen_request( } /* Apply proxy and server credentials to the request */ - if (apply_credentials(buf, &t->proxy, AUTH_HEADER_PROXY) < 0 || - apply_credentials(buf, &t->server, AUTH_HEADER_SERVER) < 0) + 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"); @@ -308,6 +312,12 @@ static int on_header_ready(http_subtransport *t) GITERR_CHECK_ALLOC(t->content_type); } } + else if (!strcasecmp("Content-Length", git_buf_cstr(name))) { + if (!t->content_length) { + t->content_length = git__strdup(git_buf_cstr(value)); + GITERR_CHECK_ALLOC(t->content_length); + } + } else if (!strcasecmp("Proxy-Authenticate", git_buf_cstr(name))) { char *dup = git__strdup(git_buf_cstr(value)); GITERR_CHECK_ALLOC(dup); @@ -438,7 +448,7 @@ static int on_headers_complete(http_parser *parser) int proxy_auth_types = 0, server_auth_types = 0; /* Enforce a reasonable cap on the number of replays */ - if (s->replay_count++ >= GIT_HTTP_REPLAY_MAX) { + if (t->replay_count++ >= GIT_HTTP_REPLAY_MAX) { giterr_set(GITERR_NET, "too many redirects or authentication replays"); return t->parse_error = PARSE_ERROR_GENERIC; } @@ -567,15 +577,19 @@ static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len) if (t->parse_error == PARSE_ERROR_REPLAY) return 0; - if (ctx->buf_size < len) { - giterr_set(GITERR_NET, "can't fit data in the buffer"); - return t->parse_error = PARSE_ERROR_GENERIC; + /* If there's no buffer set, we're explicitly ignoring the body. */ + if (ctx->buffer) { + if (ctx->buf_size < len) { + giterr_set(GITERR_NET, "can't fit data in the buffer"); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + memcpy(ctx->buffer, str, len); + ctx->buffer += len; + ctx->buf_size -= len; } - memcpy(ctx->buffer, str, len); *(ctx->bytes_read) += len; - ctx->buffer += len; - ctx->buf_size -= len; return 0; } @@ -601,6 +615,9 @@ static void clear_parser_state(http_subtransport *t) 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; @@ -734,6 +751,172 @@ static int stream_connect( return error; } +static int gen_connect_req(git_buf *buf, http_subtransport *t) +{ + git_buf_printf(buf, "CONNECT %s:%s HTTP/1.1\r\n", + t->server.url.host, t->server.url.port); + + git_buf_puts(buf, "User-Agent: "); + git_http__user_agent(buf); + git_buf_puts(buf, "\r\n"); + + git_buf_printf(buf, "Host: %s\r\n", t->proxy.url.host); + + if (apply_credentials(buf, &t->proxy, AUTH_HEADER_PROXY) < 0) + return -1; + + git_buf_puts(buf, "\r\n"); + + return git_buf_oom(buf) ? -1 : 0; +} + +static int proxy_headers_complete(http_parser *parser) +{ + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; + int proxy_auth_types = 0; + + /* Enforce a reasonable cap on the number of replays */ + if (t->replay_count++ >= GIT_HTTP_REPLAY_MAX) { + giterr_set(GITERR_NET, "too many redirects or authentication replays"); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + /* 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; + + /* + * 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, &proxy_auth_types) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; + + /* Check for a proxy authentication failure. */ + if (parser->status_code == 407) + return on_auth_required(&t->proxy.cred, + parser, + t->proxy_opts.url, + SERVER_TYPE_PROXY, + t->proxy_opts.credentials, + t->proxy_opts.payload, + t->proxy.url.user, + proxy_auth_types); + + if (parser->status_code != 200) { + giterr_set(GITERR_NET, "unexpected status code from proxy: %d", + parser->status_code); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + if (!t->content_length || strcmp(t->content_length, "0") == 0) + t->parse_finished = 1; + + return 0; +} + +static int proxy_connect( + git_stream **out, git_stream *proxy_stream, http_subtransport *t) +{ + git_buf request = GIT_BUF_INIT; + static http_parser_settings proxy_parser_settings = {0}; + size_t bytes_read = 0, bytes_parsed; + parser_context ctx; + 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); + + 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(proxy_stream, + request.ptr, request.size, 0)) < 0) + goto done; + + git_buf_dispose(&request); + + while (!bytes_read && !t->parse_finished) { + t->parse_buffer.offset = 0; + + if ((error = gitno_recv(&t->parse_buffer)) < 0) + goto done; + + /* + * 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) { + giterr_set(GITERR_NET, "proxy server sent unsupported redirect during CONNECT"); + error = -1; + goto done; + } + + /* Replay the request with authentication headers. */ + if (PARSE_ERROR_REPLAY == t->parse_error) + goto replay; + + if (t->parse_error < 0) { + error = t->parse_error == PARSE_ERROR_EXT ? PARSE_ERROR_EXT : -1; + goto done; + } + + if (bytes_parsed != t->parse_buffer.offset) { + giterr_set(GITERR_NET, + "HTTP parser error: %s", + http_errno_description((enum http_errno)t->parser.http_errno)); + error = -1; + goto done; + } + } + + if ((error = git_tls_stream_wrap(out, proxy_stream, t->server.url.host)) == 0) + error = stream_connect(*out, &t->server.url, + t->owner->certificate_check_cb, + t->owner->message_cb_payload); + + /* + * Since we've connected via a HTTPS proxy tunnel, we don't behave + * as if we have an HTTP proxy. + */ + t->proxy_opts.type = GIT_PROXY_NONE; + t->replay_count = 0; + +done: + return error; +} + static int http_connect(http_subtransport *t) { gitno_connection_data *url; @@ -751,9 +934,16 @@ static int http_connect(http_subtransport *t) git_stream_close(t->server.stream); git_stream_free(t->server.stream); t->server.stream = NULL; - t->connected = 0; } + if (t->proxy.stream) { + git_stream_close(t->proxy.stream); + git_stream_free(t->proxy.stream); + t->proxy.stream = NULL; + } + + t->connected = 0; + if (t->proxy_opts.type == GIT_PROXY_SPECIFIED) { url = &t->proxy.url; cert_cb = t->proxy_opts.certificate_check; @@ -786,6 +976,21 @@ static int http_connect(http_subtransport *t) 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 && + t->server.url.use_ssl) { + proxy_stream = stream; + stream = NULL; + + if ((error = proxy_connect(&stream, proxy_stream, t)) < 0) + goto on_error; + } + + t->proxy.stream = proxy_stream; t->server.stream = stream; t->connected = 1; return 0; @@ -1226,6 +1431,12 @@ static int http_close(git_smart_subtransport *subtransport) t->server.stream = NULL; } + if (t->proxy.stream) { + git_stream_close(t->proxy.stream); + git_stream_free(t->proxy.stream); + t->proxy.stream = NULL; + } + free_cred(&t->server.cred); free_cred(&t->server.url_cred); free_cred(&t->proxy.cred); |