Skip to content

Commit 8cd79a3

Browse files
committed
proxy: match NO_PROXY formats to libcurl behaviour
We aim to be compatible with the behaviour of Git as much as possible when it comes to network settings. This enables users to setup Git proxy settings and get the same setup "for free" with GCM. Git uses libcurl to provide it's HTTP interactions. The NO_PROXY setting is used by libcurl to disable proxy settings for specific hosts. We previously attempted to plumb the value of NO_PROXY through to the .NET WebProxy class' list of "bypassed addresses" (the set of hosts that should not be proxied). However, the .NET class expects a set of _regular expressions_ which is unlike libcurl! As a result, libcurl permitted values for NO_PROXY were throwing errors inside of GCM since they are not valid regexs. In this commit we perform a transformation of the NO_PROXY list and construct a set of regular expressions that match addresses in the same way as libcurl does. The transformation is as follows: 1. strip any leading periods '.' or wildcards '*.' 2. escape the remaining input to match literally (e.g.: '.' becomes '\.') 3. prepend a group that matches either a period '.' or a URI scheme delimiter '://' - this prevents partial domain matching 4. append a end-of-string symbol '$' to ensure we only match to the specified TLD and port See the libcurl documentation on NO_PROXY behaviour: https://curl.se/libcurl/c/CURLOPT_NOPROXY.html
1 parent 5aa3590 commit 8cd79a3

File tree

5 files changed

+223
-73
lines changed

5 files changed

+223
-73
lines changed

docs/netconfig.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,28 @@ addresses. GCM Core supports the cURL environment variable `NO_PROXY` for this
6262
scenariom, as does Git itself.
6363

6464
The `NO_PROXY` environment variable should contain a comma (`,`) or space (` `)
65-
separated list of regular expressions to match hosts that should not be proxied
66-
(should connect directly).
65+
separated list of host names that should not be proxied (should connect
66+
directly).
67+
68+
GCM Core attempts to match [libcurl's behaviour](https://curl.se/libcurl/c/CURLOPT_NOPROXY.html),
69+
which is briefly summarized here:
70+
71+
- a value of `*` disables proxying for all hosts;
72+
- other wildcard use is **not** supported;
73+
- each name in the list is matched as a domain which contains the hostname,
74+
or the hostname itself
75+
- a leading period/dot `.` matches against the provided hostname
76+
77+
For example, setting `NO_PROXY` to `example.com` results in the following:
78+
79+
Hostname|Matches?
80+
-|-
81+
`example.com`|:white_check_mark:
82+
`example.com:80`|:white_check_mark:
83+
`www.example.com`|:white_check_mark:
84+
`notanexample.com`|:x:
85+
`www.notanexample.com`|:x:
86+
`example.com.othertld`|:x:
6787

6888
**Example:**
6989

src/shared/Core.Tests/HttpClientFactoryTests.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,13 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithBypass_ReturnsTrueOutProxy
8888
const string repoPath = "/tmp/repos/foo";
8989
const string repoRemote = "https://remote.example.com/foo.git";
9090

91-
var bypassList = new List<string> {"https://contoso.com", ".*fabrikam\\.com"};
91+
var noProxyRaw = "contoso.com,fabrikam.com";
9292
var repoRemoteUri = new Uri(repoRemote);
9393
var proxyConfig = new ProxyConfiguration(
9494
new Uri(proxyUrl),
9595
userName: null,
9696
password: null,
97-
bypassHosts: bypassList);
97+
noProxyRaw: noProxyRaw);
9898

9999
var settings = new TestSettings
100100
{
@@ -117,6 +117,35 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithBypass_ReturnsTrueOutProxy
117117
Assert.False(proxy.IsBypassed(repoRemoteUri));
118118
}
119119

120+
[Fact]
121+
public void HttpClientFactory_TryCreateProxy_ProxyWithWildcardBypass_ReturnsFalse()
122+
{
123+
const string proxyUrl = "https://proxy.example.com/git";
124+
const string repoPath = "/tmp/repos/foo";
125+
const string repoRemote = "https://remote.example.com/foo.git";
126+
127+
var noProxyRaw = "*";
128+
var repoRemoteUri = new Uri(repoRemote);
129+
var proxyConfig = new ProxyConfiguration(
130+
new Uri(proxyUrl),
131+
userName: null,
132+
password: null,
133+
noProxyRaw: noProxyRaw);
134+
135+
var settings = new TestSettings
136+
{
137+
RemoteUri = repoRemoteUri,
138+
RepositoryPath = repoPath,
139+
ProxyConfiguration = proxyConfig
140+
};
141+
var httpFactory = new HttpClientFactory(Mock.Of<IFileSystem>(), Mock.Of<ITrace>(), settings, Mock.Of<IStandardStreams>());
142+
143+
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
144+
145+
Assert.False(result);
146+
Assert.Null(proxy);
147+
}
148+
120149
[Fact]
121150
public void HttpClientFactory_TryCreateProxy_ProxyWithCredentials_ReturnsTrueOutProxyWithUrlConfiguredCredentials()
122151
{

src/shared/Core.Tests/SettingsTests.cs

Lines changed: 73 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Net;
45
using GitCredentialManager.Tests.Objects;
56
using Xunit;
67

@@ -427,6 +428,57 @@ public void Settings_IsWindowsIntegratedAuthenticationEnabled_ConfigNonBooleanyV
427428
Assert.True(settings.IsWindowsIntegratedAuthenticationEnabled);
428429
}
429430

431+
[Theory]
432+
[InlineData("", new string[0])]
433+
[InlineData(" ", new string[0])]
434+
[InlineData(",", new string[0])]
435+
[InlineData("example.com", new[] { @"(\.|\:\/\/)example\.com$" })]
436+
[InlineData("example.com:8080", new[] { @"(\.|\:\/\/)example\.com:8080$" })]
437+
[InlineData("example.com,", new[] { @"(\.|\:\/\/)example\.com$" })]
438+
[InlineData(",example.com", new[] { @"(\.|\:\/\/)example\.com$" })]
439+
[InlineData(",example.com,", new[] { @"(\.|\:\/\/)example\.com$" })]
440+
[InlineData(".example.com", new[] { @"(\.|\:\/\/)example\.com$" })]
441+
[InlineData("..example.com", new[] { @"(\.|\:\/\/)example\.com$" })]
442+
[InlineData("*.example.com", new[] { @"(\.|\:\/\/)example\.com$" })]
443+
[InlineData("my.example.com", new[] { @"(\.|\:\/\/)my\.example\.com$" })]
444+
[InlineData("example.com,contoso.com,fabrikam.com", new[]
445+
{
446+
@"(\.|\:\/\/)example\.com$",
447+
@"(\.|\:\/\/)contoso\.com$",
448+
@"(\.|\:\/\/)fabrikam\.com$"
449+
})]
450+
public void Settings_ProxyConfiguration_ConvertToBypassRegexArray(string input, string[] expected)
451+
{
452+
string[] actual = ProxyConfiguration.ConvertToBypassRegexArray(input).ToArray();
453+
Assert.Equal(expected, actual);
454+
}
455+
456+
[Theory]
457+
[InlineData("example.com", "http://example.com", true)]
458+
[InlineData("example.com", "https://example.com", true)]
459+
[InlineData("example.com", "https://www.example.com", true)]
460+
[InlineData("example.com", "http://www.example.com:80", true)]
461+
[InlineData("example.com", "https://www.example.com:443", true)]
462+
[InlineData("example.com", "https://www.example.com:8080", false)]
463+
[InlineData("example.com", "http://notanexample.com", false)]
464+
[InlineData("example.com", "https://notanexample.com", false)]
465+
[InlineData("example.com", "https://www.notanexample.com", false)]
466+
[InlineData("example.com", "https://example.com.otherltd", false)]
467+
[InlineData("example.com:8080", "http://example.com", false)]
468+
[InlineData("my.example.com", "http://example.com", false)]
469+
public void Settings_ProxyConfiguration_ConvertToBypassRegexArray_WebProxyBypass(string noProxy, string address, bool expected)
470+
{
471+
var bypassList = ProxyConfiguration.ConvertToBypassRegexArray(noProxy).ToArray();
472+
var webProxy = new WebProxy("https://localhost:8080/proxy")
473+
{
474+
BypassList = bypassList
475+
};
476+
477+
bool actual = webProxy.IsBypassed(new Uri(address));
478+
479+
Assert.Equal(expected, actual);
480+
}
481+
430482
[Fact]
431483
public void Settings_ProxyConfiguration_Unset_ReturnsNull()
432484
{
@@ -458,11 +510,11 @@ public void Settings_ProxyConfiguration_GcmHttpConfig_ReturnsValue()
458510
const string expectedPassword = "letmein123";
459511
var expectedAddress = new Uri("http://proxy.example.com");
460512
var settingValue = new Uri("http://john.doe:[email protected]");
461-
var bypassList = new List<string> {"contoso.com", "fabrikam.com"};
513+
var expectedNoProxy = "contoso.com,fabrikam.com";
462514

463515
var envars = new TestEnvironment
464516
{
465-
Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = string.Join(',', bypassList)}
517+
Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy}
466518
};
467519
var git = new TestGit();
468520
git.Configuration.Global[$"{section}.{property}"] = new[] {settingValue.ToString()};
@@ -478,7 +530,7 @@ public void Settings_ProxyConfiguration_GcmHttpConfig_ReturnsValue()
478530
Assert.Equal(expectedAddress, actualConfig.Address);
479531
Assert.Equal(expectedUserName, actualConfig.UserName);
480532
Assert.Equal(expectedPassword, actualConfig.Password);
481-
Assert.Equal(bypassList, actualConfig.BypassHosts);
533+
Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);
482534
Assert.True(actualConfig.IsDeprecatedSource);
483535
}
484536

@@ -494,11 +546,11 @@ public void Settings_ProxyConfiguration_GcmHttpsConfig_ReturnsValue()
494546
const string expectedPassword = "letmein123";
495547
var expectedAddress = new Uri("http://proxy.example.com");
496548
var settingValue = new Uri("http://john.doe:[email protected]");
497-
var bypassList = new List<string> {"contoso.com", "fabrikam.com"};
549+
var expectedNoProxy = "contoso.com,fabrikam.com";
498550

499551
var envars = new TestEnvironment
500552
{
501-
Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = string.Join(',', bypassList)}
553+
Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy}
502554
};
503555
var git = new TestGit();
504556
git.Configuration.Global[$"{section}.{property}"] = new[] {settingValue.ToString()};
@@ -514,7 +566,7 @@ public void Settings_ProxyConfiguration_GcmHttpsConfig_ReturnsValue()
514566
Assert.Equal(expectedAddress, actualConfig.Address);
515567
Assert.Equal(expectedUserName, actualConfig.UserName);
516568
Assert.Equal(expectedPassword, actualConfig.Password);
517-
Assert.Equal(bypassList, actualConfig.BypassHosts);
569+
Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);
518570
Assert.True(actualConfig.IsDeprecatedSource);
519571
}
520572

@@ -530,11 +582,11 @@ public void Settings_ProxyConfiguration_GitHttpConfig_ReturnsValue()
530582
const string expectedPassword = "letmein123";
531583
var expectedAddress = new Uri("http://proxy.example.com");
532584
var settingValue = new Uri("http://john.doe:[email protected]");
533-
var bypassList = new List<string> {"contoso.com", "fabrikam.com"};
585+
var expectedNoProxy = "contoso.com,fabrikam.com";
534586

535587
var envars = new TestEnvironment
536588
{
537-
Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = string.Join(',', bypassList)}
589+
Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy}
538590
};
539591
var git = new TestGit();
540592
git.Configuration.Global[$"{section}.{property}"] = new[] {settingValue.ToString()};
@@ -550,7 +602,7 @@ public void Settings_ProxyConfiguration_GitHttpConfig_ReturnsValue()
550602
Assert.Equal(expectedAddress, actualConfig.Address);
551603
Assert.Equal(expectedUserName, actualConfig.UserName);
552604
Assert.Equal(expectedPassword, actualConfig.Password);
553-
Assert.Equal(bypassList, actualConfig.BypassHosts);
605+
Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);
554606
Assert.False(actualConfig.IsDeprecatedSource);
555607
}
556608

@@ -579,42 +631,6 @@ public void Settings_ProxyConfiguration_GitHttpConfig_EmptyScopedUriUnscoped_Ret
579631
Assert.Null(actualConfig);
580632
}
581633

582-
[Fact]
583-
public void Settings_ProxyConfiguration_NoProxyMixedSplitChar_ReturnsValue()
584-
{
585-
const string remoteUrl = "http://example.com/foo.git";
586-
const string section = Constants.GitConfiguration.Http.SectionName;
587-
const string property = Constants.GitConfiguration.Http.Proxy;
588-
var remoteUri = new Uri(remoteUrl);
589-
590-
const string expectedUserName = "john.doe";
591-
const string expectedPassword = "letmein123";
592-
var expectedAddress = new Uri("http://proxy.example.com");
593-
var settingValue = new Uri("http://john.doe:[email protected]");
594-
var bypassList = new List<string> {"contoso.com", "fabrikam.com", "example.com"};
595-
596-
var envars = new TestEnvironment
597-
{
598-
Variables = {[Constants.EnvironmentVariables.CurlNoProxy] = "contoso.com, fabrikam.com example.com,"}
599-
};
600-
var git = new TestGit();
601-
git.Configuration.Global[$"{section}.{property}"] = new[] {settingValue.ToString()};
602-
603-
var settings = new Settings(envars, git)
604-
{
605-
RemoteUri = remoteUri
606-
};
607-
608-
ProxyConfiguration actualConfig = settings.GetProxyConfiguration();
609-
610-
Assert.NotNull(actualConfig);
611-
Assert.Equal(expectedAddress, actualConfig.Address);
612-
Assert.Equal(expectedUserName, actualConfig.UserName);
613-
Assert.Equal(expectedPassword, actualConfig.Password);
614-
Assert.Equal(bypassList, actualConfig.BypassHosts);
615-
Assert.False(actualConfig.IsDeprecatedSource);
616-
}
617-
618634
[Fact]
619635
public void Settings_ProxyConfiguration_CurlHttpEnvar_ReturnsValue()
620636
{
@@ -625,14 +641,14 @@ public void Settings_ProxyConfiguration_CurlHttpEnvar_ReturnsValue()
625641
const string expectedPassword = "letmein123";
626642
var expectedAddress = new Uri("http://proxy.example.com");
627643
var settingValue = new Uri("http://john.doe:[email protected]");
628-
var bypassList = new List<string> {"contoso.com", "fabrikam.com"};
644+
var expectedNoProxy = "contoso.com,fabrikam.com";
629645

630646
var envars = new TestEnvironment
631647
{
632648
Variables =
633649
{
634650
[Constants.EnvironmentVariables.CurlHttpProxy] = settingValue.ToString(),
635-
[Constants.EnvironmentVariables.CurlNoProxy] = string.Join(',', bypassList)
651+
[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy
636652
}
637653
};
638654
var git = new TestGit();
@@ -648,7 +664,7 @@ public void Settings_ProxyConfiguration_CurlHttpEnvar_ReturnsValue()
648664
Assert.Equal(expectedAddress, actualConfig.Address);
649665
Assert.Equal(expectedUserName, actualConfig.UserName);
650666
Assert.Equal(expectedPassword, actualConfig.Password);
651-
Assert.Equal(bypassList, actualConfig.BypassHosts);
667+
Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);
652668
Assert.False(actualConfig.IsDeprecatedSource);
653669
}
654670

@@ -662,14 +678,14 @@ public void Settings_ProxyConfiguration_CurlHttpsEnvar_ReturnsValue()
662678
const string expectedPassword = "letmein123";
663679
var expectedAddress = new Uri("http://proxy.example.com");
664680
var settingValue = new Uri("http://john.doe:[email protected]");
665-
var bypassList = new List<string> {"contoso.com", "fabrikam.com"};
681+
var expectedNoProxy = "contoso.com,fabrikam.com";
666682

667683
var envars = new TestEnvironment
668684
{
669685
Variables =
670686
{
671687
[Constants.EnvironmentVariables.CurlHttpsProxy] = settingValue.ToString(),
672-
[Constants.EnvironmentVariables.CurlNoProxy] = string.Join(',', bypassList)
688+
[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy
673689
}
674690
};
675691
var git = new TestGit();
@@ -685,7 +701,7 @@ public void Settings_ProxyConfiguration_CurlHttpsEnvar_ReturnsValue()
685701
Assert.Equal(expectedAddress, actualConfig.Address);
686702
Assert.Equal(expectedUserName, actualConfig.UserName);
687703
Assert.Equal(expectedPassword, actualConfig.Password);
688-
Assert.Equal(bypassList, actualConfig.BypassHosts);
704+
Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);
689705
Assert.False(actualConfig.IsDeprecatedSource);
690706
}
691707

@@ -699,14 +715,14 @@ public void Settings_TryGetProxy_CurlAllEnvar_ReturnsValue()
699715
const string expectedPassword = "letmein123";
700716
var expectedAddress = new Uri("http://proxy.example.com");
701717
var settingValue = new Uri("http://john.doe:[email protected]");
702-
var bypassList = new List<string> {"contoso.com", "fabrikam.com"};
718+
var expectedNoProxy = "contoso.com,fabrikam.com";
703719

704720
var envars = new TestEnvironment
705721
{
706722
Variables =
707723
{
708724
[Constants.EnvironmentVariables.CurlAllProxy] = settingValue.ToString(),
709-
[Constants.EnvironmentVariables.CurlNoProxy] = string.Join(',', bypassList)
725+
[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy
710726
}
711727
};
712728
var git = new TestGit();
@@ -722,7 +738,7 @@ public void Settings_TryGetProxy_CurlAllEnvar_ReturnsValue()
722738
Assert.Equal(expectedAddress, actualConfig.Address);
723739
Assert.Equal(expectedUserName, actualConfig.UserName);
724740
Assert.Equal(expectedPassword, actualConfig.Password);
725-
Assert.Equal(bypassList, actualConfig.BypassHosts);
741+
Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);
726742
Assert.False(actualConfig.IsDeprecatedSource);
727743
}
728744

@@ -736,14 +752,14 @@ public void Settings_ProxyConfiguration_LegacyGcmEnvar_ReturnsValue()
736752
const string expectedPassword = "letmein123";
737753
var expectedAddress = new Uri("http://proxy.example.com");
738754
var settingValue = new Uri("http://john.doe:[email protected]");
739-
var bypassList = new List<string> {"https://contoso.com", ".*fabrikam\\.com"};
755+
var expectedNoProxy = "contoso.com,fabrikam.com";
740756

741757
var envars = new TestEnvironment
742758
{
743759
Variables =
744760
{
745761
[Constants.EnvironmentVariables.GcmHttpProxy] = settingValue.ToString(),
746-
[Constants.EnvironmentVariables.CurlNoProxy] = string.Join(',', bypassList)
762+
[Constants.EnvironmentVariables.CurlNoProxy] = expectedNoProxy
747763
}
748764
};
749765
var git = new TestGit();
@@ -759,7 +775,7 @@ public void Settings_ProxyConfiguration_LegacyGcmEnvar_ReturnsValue()
759775
Assert.Equal(expectedAddress, actualConfig.Address);
760776
Assert.Equal(expectedUserName, actualConfig.UserName);
761777
Assert.Equal(expectedPassword, actualConfig.Password);
762-
Assert.Equal(bypassList, actualConfig.BypassHosts);
778+
Assert.Equal(expectedNoProxy, actualConfig.NoProxyRaw);
763779
Assert.True(actualConfig.IsDeprecatedSource);
764780
}
765781

0 commit comments

Comments
 (0)