Commit e5a3277452095a908787c3ea0279fbec21c0a76a

Mathieu Parent 2021-02-11T22:53:16

Add NO_PROXY env support Item 2 of 3 from #4164 Signed-off-by: Mathieu Parent <math.parent@gmail.com>

diff --git a/src/remote.c b/src/remote.c
index 73375b3..e63f54a 100644
--- a/src/remote.c
+++ b/src/remote.c
@@ -849,11 +849,70 @@ int git_remote_ls(const git_remote_head ***out, size_t *size, git_remote *remote
 	return remote->transport->ls(out, size, remote->transport);
 }
 
-int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, char **proxy_url)
+int git_remote__get_http_proxy_bypass(git_net_url *url, git_buf *no_proxy_env, bool *bypass)
+{
+	int error = 0;
+	char *p_start = no_proxy_env->ptr;
+	size_t p_length = 0;
+	char c;
+	git_buf hostport = GIT_BUF_INIT;
+
+	error = git_buf_printf(&hostport, "%s:%s", url->host, url->port);
+	if (error < 0)
+		return error;
+
+	*bypass = false;
+
+	do {
+		c = *(p_start + p_length);
+		if ((c == ',') || (c == 0)) {
+			if ((p_length == 1) && (*p_start == '*')) {
+				// wildcard match (*)
+				goto found;
+			} else if ((p_length == strlen(url->host)) && !memcmp(p_start, url->host, p_length)) {
+				// exact host match
+				goto found;
+			} else if ((p_length == strlen(hostport.ptr)) && !memcmp(p_start, hostport.ptr, p_length)) {
+				// exact host:port match
+				goto found;
+			} else {
+				if ((p_length >= 2) && (*p_start == '*') && (*(p_start + 1) == '.')) {
+					// *.foo == .foo
+					p_start++;
+					p_length--;
+				}
+				if ((*p_start == '.') && (strlen(url->host) > p_length) && !memcmp(p_start, url->host + strlen(url->host) - p_length, p_length)) {
+					// host suffix match (.example.org)
+					goto found;
+				} else if ((*p_start == '.') && (strlen(hostport.ptr) > p_length) && !memcmp(p_start, hostport.ptr + strlen(hostport.ptr) - p_length, p_length)) {
+					// host:port suffix match (.example.org:443)
+					goto found;
+				}
+			}
+			p_start += p_length + 1;
+			p_length = 0;
+		} else {
+			p_length++;
+		}
+	} while(c != 0);
+
+	goto end;
+
+found:
+	*bypass = true;
+
+end:
+	git_buf_dispose(&hostport);
+	return 0;
+}
+
+int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, git_net_url *url, char **proxy_url)
 {
 	git_config *cfg;
 	git_config_entry *ce = NULL;
-	git_buf val = GIT_BUF_INIT;
+	git_buf proxy_env = GIT_BUF_INIT;
+	git_buf no_proxy_env = GIT_BUF_INIT;
+	bool bypass = false;
 	int error;
 
 	GIT_ASSERT_ARG(remote);
@@ -898,11 +957,11 @@ int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, char **proxy_ur
 	}
 
 	/* http_proxy / https_proxy environment variables */
-	error = git__getenv(&val, use_ssl ? "https_proxy" : "http_proxy");
+	error = git__getenv(&proxy_env, use_ssl ? "https_proxy" : "http_proxy");
 
 	/* try uppercase environment variables */
 	if (error == GIT_ENOTFOUND)
-		error = git__getenv(&val, use_ssl ? "HTTPS_PROXY" : "HTTP_PROXY");
+		error = git__getenv(&proxy_env, use_ssl ? "HTTPS_PROXY" : "HTTP_PROXY");
 
 	if (error < 0) {
 		if (error == GIT_ENOTFOUND) {
@@ -913,13 +972,35 @@ int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, char **proxy_ur
 		return error;
 	}
 
-	*proxy_url = git_buf_detach(&val);
+	/* no_proxy/NO_PROXY environment variables */
+	error = git__getenv(&no_proxy_env, "no_proxy");
+	if (error == GIT_ENOTFOUND)
+		error = git__getenv(&no_proxy_env, "NO_PROXY");
+
+	if (error == GIT_ENOTFOUND) {
+		git_error_clear();
+		error = 0;
+	} else if (error < 0) {
+		goto cleanup;
+	} else {
+		error = git_remote__get_http_proxy_bypass(url, &no_proxy_env, &bypass);
+	}
+
+	if (bypass) {
+		git_buf_dispose(&proxy_env);
+		goto cleanup;
+	} else {
+		*proxy_url = git_buf_detach(&proxy_env);
+	}
 
 found:
 	GIT_ERROR_CHECK_ALLOC(*proxy_url);
+
+cleanup:
+	git_buf_dispose(&no_proxy_env);
 	git_config_entry_free(ce);
 
-	return 0;
+	return error;
 }
 
 /* DWIM `refspecs` based on `refs` and append the output to `out` */
diff --git a/src/remote.h b/src/remote.h
index df75ed3..ffcefdf 100644
--- a/src/remote.h
+++ b/src/remote.h
@@ -9,6 +9,7 @@
 
 #include "common.h"
 
+#include "net.h"
 #include "git2/remote.h"
 #include "git2/transport.h"
 #include "git2/sys/transport.h"
@@ -46,7 +47,8 @@ typedef struct git_remote_connection_opts {
 int git_remote__connect(git_remote *remote, git_direction direction, const git_remote_callbacks *callbacks, const git_remote_connection_opts *conn);
 
 int git_remote__urlfordirection(git_buf *url_out, struct git_remote *remote, int direction, const git_remote_callbacks *callbacks);
-int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, char **proxy_url);
+int git_remote__get_http_proxy_bypass(git_net_url *url, git_buf *no_proxy_env, bool *bypass);
+int git_remote__get_http_proxy(git_remote *remote, bool use_ssl, git_net_url *url, char **proxy_url);
 
 git_refspec *git_remote__matching_refspec(git_remote *remote, const char *refname);
 git_refspec *git_remote__matching_dst_refspec(git_remote *remote, const char *refname);
diff --git a/src/transports/http.c b/src/transports/http.c
index 691bceb..5468674 100644
--- a/src/transports/http.c
+++ b/src/transports/http.c
@@ -306,7 +306,7 @@ static int lookup_proxy(
 		remote = transport->owner->owner;
 		use_ssl = !strcmp(transport->server.url.scheme, "https");
 
-		error = git_remote__get_http_proxy(remote, use_ssl, &config);
+		error = git_remote__get_http_proxy(remote, use_ssl, &transport->server.url, &config);
 
 		if (error || !config)
 			goto done;
diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c
index ea2195a..8dc39d8 100644
--- a/src/transports/winhttp.c
+++ b/src/transports/winhttp.c
@@ -373,6 +373,7 @@ static int winhttp_stream_connect(winhttp_stream *s)
 {
 	winhttp_subtransport *t = OWNING_SUBTRANSPORT(s);
 	git_buf buf = GIT_BUF_INIT;
+	bool use_ssl;
 	char *proxy_url = NULL;
 	wchar_t ct[MAX_CONTENT_TYPE_LEN];
 	LPCWSTR types[] = { L"*/*", NULL };
@@ -429,7 +430,8 @@ static int winhttp_stream_connect(winhttp_stream *s)
 	proxy_opts = &t->owner->proxy;
 	if (proxy_opts->type == GIT_PROXY_AUTO) {
 		/* Set proxy if necessary */
-		if (git_remote__get_http_proxy(t->owner->owner, (strcmp(t->server.url.scheme, "https") == 0), &proxy_url) < 0)
+		use_ssl = strcmp(t->server.url.scheme, "https") == 0;
+		if (git_remote__get_http_proxy(t->owner->owner, use_ssl, &t->server.url, &proxy_url) < 0)
 			goto on_error;
 	}
 	else if (proxy_opts->type == GIT_PROXY_SPECIFIED) {
diff --git a/tests/online/clone.c b/tests/online/clone.c
index dbf45dc..d9b1837 100644
--- a/tests/online/clone.c
+++ b/tests/online/clone.c
@@ -36,6 +36,7 @@ static char *_remote_expectcontinue = NULL;
 static int _orig_proxies_need_reset = 0;
 static char *_orig_http_proxy = NULL;
 static char *_orig_https_proxy = NULL;
+static char *_orig_no_proxy = NULL;
 
 static int ssl_cert(git_cert *cert, int valid, const char *host, void *payload)
 {
@@ -110,9 +111,11 @@ void test_online_clone__cleanup(void)
 	if (_orig_proxies_need_reset) {
 		cl_setenv("HTTP_PROXY", _orig_http_proxy);
 		cl_setenv("HTTPS_PROXY", _orig_https_proxy);
+		cl_setenv("NO_PROXY", _orig_no_proxy);
 
 		git__free(_orig_http_proxy);
 		git__free(_orig_https_proxy);
+		git__free(_orig_no_proxy);
 	}
 
 	git_libgit2_opts(GIT_OPT_SET_SSL_CERT_LOCATIONS, NULL, NULL);
@@ -854,6 +857,7 @@ void test_online_clone__proxy_credentials_in_environment(void)
 
 	_orig_http_proxy = cl_getenv("HTTP_PROXY");
 	_orig_https_proxy = cl_getenv("HTTPS_PROXY");
+	_orig_no_proxy = cl_getenv("NO_PROXY");
 	_orig_proxies_need_reset = 1;
 
 	g_options.fetch_opts.proxy_opts.type = GIT_PROXY_AUTO;
@@ -865,6 +869,7 @@ void test_online_clone__proxy_credentials_in_environment(void)
 
 	cl_setenv("HTTP_PROXY", url.ptr);
 	cl_setenv("HTTPS_PROXY", url.ptr);
+	cl_setenv("NO_PROXY", NULL);
 
 	cl_git_pass(git_clone(&g_repo, "http://github.com/libgit2/TestGitRepository", "./foo", &g_options));
 
@@ -893,6 +898,67 @@ void test_online_clone__proxy_credentials_in_url_https(void)
 	git_buf_dispose(&url);
 }
 
+struct no_proxy_test_entry {
+	char no_proxy[128];
+	bool bypass;
+};
+
+static struct no_proxy_test_entry no_proxy_test_entries[] = {
+	{"*", true},
+	{"github.com", true},
+	{"github.com:443", true},
+	{"github.com:80", false},
+	{".github.com", false},
+	{"*.github.com", false},
+	{".com", true},
+	{"*.com", true},
+	{".com:443", true},
+	{"*.com:443", true},
+	{".com:80", false},
+	{"*.com:80", false},
+	{"", false}
+};
+
+void test_online_clone__no_proxy_in_environment(void)
+{
+	int error = 0;
+	unsigned int i;
+	git_buf proxy_url = GIT_BUF_INIT;
+
+	_orig_http_proxy = cl_getenv("HTTP_PROXY");
+	_orig_https_proxy = cl_getenv("HTTPS_PROXY");
+	_orig_no_proxy = cl_getenv("NO_PROXY");
+	_orig_proxies_need_reset = 1;
+
+	g_options.fetch_opts.proxy_opts.type = GIT_PROXY_AUTO;
+	g_options.fetch_opts.proxy_opts.certificate_check = proxy_cert_cb;
+
+	cl_git_pass(git_buf_printf(&proxy_url, "http://does-not-exists.example.org:1234/"));
+
+	cl_setenv("HTTP_PROXY", proxy_url.ptr);
+	cl_setenv("HTTPS_PROXY", proxy_url.ptr);
+
+
+	for (i = 0; i < ARRAY_SIZE(no_proxy_test_entries); ++i) {
+		cl_setenv("NO_PROXY", no_proxy_test_entries[i].no_proxy);
+		error = git_clone(&g_repo, "https://github.com/libgit2/TestGitRepository", "./foo", &g_options);
+
+		if (no_proxy_test_entries[i].bypass) {
+			cl_assert_(error == 0, no_proxy_test_entries[i].no_proxy);
+		} else {
+			cl_assert_(error == -1, no_proxy_test_entries[i].no_proxy);
+		}
+
+		if (g_repo) {
+			git_repository_free(g_repo);
+			g_repo = NULL;
+		}
+		cl_fixture_cleanup("./foo");
+	}
+
+	git_buf_dispose(&proxy_url);
+}
+
 void test_online_clone__proxy_auto_not_detected(void)
 {
 	g_options.fetch_opts.proxy_opts.type = GIT_PROXY_AUTO;
diff --git a/tests/remote/no_proxy.c b/tests/remote/no_proxy.c
new file mode 100644
index 0000000..4f75841
--- /dev/null
+++ b/tests/remote/no_proxy.c
@@ -0,0 +1,40 @@
+#include "clar_libgit2.h"
+#include "remote.h"
+
+/* Suite data */
+struct no_proxy_test_entry {
+	char url[128];
+	char no_proxy[128];
+	bool bypass;
+};
+
+static struct no_proxy_test_entry no_proxy_test_entries[] = {
+	{"https://example.com/", "", false},
+	{"https://example.com/", "example.org", false},
+	{"https://example.com/", "*", true},
+	{"https://example.com/", "example.com,example.org", true},
+	{"https://example.com/", ".example.com,example.org", false},
+	{"https://foo.example.com/", ".example.com,example.org", true},
+	{"https://example.com/", "foo.example.com,example.org", false},
+
+};
+
+void test_remote_no_proxy__entries(void)
+{
+	unsigned int i;
+	git_net_url url = GIT_NET_URL_INIT;
+	git_buf no_proxy = GIT_BUF_INIT;
+	bool bypass = false;
+
+	for (i = 0; i < ARRAY_SIZE(no_proxy_test_entries); ++i) {
+		cl_git_pass(git_net_url_parse(&url, no_proxy_test_entries[i].url));
+		cl_git_pass(git_buf_sets(&no_proxy, no_proxy_test_entries[i].no_proxy));
+		cl_git_pass(git_remote__get_http_proxy_bypass(&url, &no_proxy, &bypass));
+
+		cl_assert_(bypass == no_proxy_test_entries[i].bypass, no_proxy_test_entries[i].no_proxy);
+
+		git_net_url_dispose(&url);
+		git_buf_dispose(&no_proxy);
+	}
+
+}