Skip to content

Commit 4659c50

Browse files
authored
Merge pull request #62 from mjcheetham/proxy
Use Git's HTTP proxy configuration
2 parents 3c215eb + 252a041 commit 4659c50

31 files changed

+1032
-124
lines changed

Git-Credential-Manager.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ ProjectSection(SolutionItems) = preProject
6363
docs\faq.md = docs\faq.md
6464
docs\migration.md = docs\migration.md
6565
docs\usage.md = docs\usage.md
66+
docs\netconfig.md = docs\netconfig.md
6667
EndProjectSection
6768
EndProject
6869
Global

docs/configuration.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,21 @@ git config --global credential.tfsonprem123.allowWindowsAuth false
9292
```
9393

9494
**Also see: [GCM_ALLOW_WINDOWSAUTH](environment.md#GCM_ALLOW_WINDOWSAUTH)**
95+
96+
---
97+
98+
### credential.httpProxy _(deprecated)_
99+
100+
> This setting is deprecated and should be replaced by the [standard `http.proxy` Git configuration option](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy).
101+
>
102+
> Click [here](https://aka.ms/gcmcore-httpproxy) for more information.
103+
104+
Configure GCM Core to use the a proxy for network operations.
105+
106+
**Note:** Git itself does _not_ respect this setting; this affects GCM _only_.
107+
108+
```shell
109+
git config --global credential.httpsProxy http://john.doe:[email protected]
110+
```
111+
112+
**Also see: [GCM_HTTP_PROXY](environment.md#GCM_HTTP_PROXY-deprecated)**

docs/environment.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ Defaults to disabled.
120120
_No configuration equivalent._
121121

122122
---
123+
123124
### GCM_PROVIDER
124125

125126
Define the host provider to use when authenticating.
@@ -214,3 +215,29 @@ export GCM_ALLOW_WINDOWSAUTH=0
214215
```
215216

216217
**Also see: [credential.allowWindowsAuth](environment.md#credentialallowWindowsAuth)**
218+
219+
---
220+
221+
### GCM_HTTP_PROXY _(deprecated)_
222+
223+
> This setting is deprecated and should be replaced by the [standard `http.proxy` Git configuration option](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy).
224+
>
225+
> Click [here](https://aka.ms/gcmcore-httpproxy) for more information.
226+
227+
Configure GCM Core to use the a proxy for network operations.
228+
229+
**Note:** Git itself does _not_ respect this setting; this affects GCM _only_.
230+
231+
##### Windows
232+
233+
```batch
234+
SET GCM_HTTP_PROXY=http://john.doe:[email protected]
235+
```
236+
237+
##### macOS/Linux
238+
239+
```bash
240+
export GCM_HTTP_PROXY=http://john.doe:[email protected]
241+
```
242+
243+
**Also see: [credential.httpProxy](configuration.md#credentialhttpProxy-deprecated)**

docs/netconfig.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Network and HTTP configuration
2+
3+
Git Credential Manager Core's network and HTTP(S) behavior can be configured in a few different ways via [environment variables](environment.md) and [configuration options](configuration.md).
4+
5+
## HTTP Proxy
6+
7+
If your computer sits behind a network firewall that requires the use of a proxy server to reach repository remotes or the wider Internet, there are various methods for configuring GCM to use a proxy.
8+
9+
The simplist way to configure a proxy for _all_ HTTP(S) remotes is to [use the standard Git HTTP(S) proxy setting `http.proxy`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpproxy).
10+
11+
For example to configure a proxy for all remotes for the current user:
12+
13+
```shell
14+
git config --global http.proxy http://proxy.example.com
15+
```
16+
17+
To specify a proxy for a particular remote you can [use the `remote.<name>.proxy` repository-level setting](https://git-scm.com/docs/git-config#Documentation/git-config.txt-remoteltnamegtproxy), for example:
18+
19+
```shell
20+
git config --local remote.origin.proxy http://proxy.example.com
21+
```
22+
23+
The advantage to using these standard configuration options is that in addition to GCM being configured to use the proxy, Git itself will be configured at the same time. This is probably the most commonly desired case in environments behind an Internet-blocking firewall.
24+
25+
### Authenticated proxies
26+
27+
Some proxy servers do not accept anonymous connections and require authentication. In order to specify the credentials to be used with a proxy, you can specify the username and password as part of the proxy URL setting.
28+
29+
The format follows [RFC 3986 section 3.2.1](https://tools.ietf.org/html/rfc3986#section-3.2.1) by including the credentials in the 'user information' part of the URI. The password is optional.
30+
31+
```text
32+
protocol://username[:password]@hostname
33+
```
34+
35+
For example, to specify the username `john.doe` and the password `letmein123` for the proxy server `proxy.example.com`:
36+
37+
```text
38+
https://john.doe:[email protected]
39+
```
40+
41+
If you have special characters (as defined by [RFC 3986 section 2.2](https://tools.ietf.org/html/rfc3986#section-2.2)) in your username or password such as `:`, `@`, or any other non-URL friendly character you can URL-encode them ([section 2.1](https://tools.ietf.org/html/rfc3986#section-2.2)).
42+
43+
For example, a space character would be encoded with `%20`.
44+
45+
### Other proxy options
46+
47+
GCM Core supports other ways of configuring a proxy for convenience and compatibility.
48+
49+
1. GCM-specific configuration options (_**only** respected by GCM; **deprecated**_):
50+
- `credential.httpProxy`
51+
- `credential.httpsProxy`
52+
1. cURL environment variables (_also respected by Git_):
53+
- `HTTP_PROXY`
54+
- `HTTPS_PROXY`
55+
- `ALL_PROXY`
56+
1. `GCM_HTTP_PROXY` environment variable (_**only** respected by GCM; **deprecated**_)
57+
58+
## TLS Verification
59+
60+
If you are using self-signed TLS (SSL) certificates with a self-hosted host provider such as GitHub Enteprise Server or Azure DevOps Server (previously TFS), you may see the following error message when attempting to connect using Git and/or GCM:
61+
62+
```shell
63+
$ git clone https://ghe.example.com/john.doe/myrepo
64+
fatal: The remote certificate is invalid according to the validation procedure.
65+
```
66+
67+
The **recommended and safest option** is to acquire a TLS certificate signed by a public trusted certificate authority (CA). There are multiple public CAs; here is a non-exhaustive list to consider: [Let's Encrypt](https://letsencrypt.org/), [Comodo](https://www.comodoca.com/), [Digicert](https://www.digicert.com/), [GoDaddy](https://www.godaddy.com/web-security/ssl-certificate), [GlobalSign](https://www.globalsign.com/en/ssl/).
68+
69+
If it is not possible to **obtain a TLS certifiate from a trusted 3rd party** then you should try to add the _specific_ self-signed certificate or one of the CA certificates in the verification chain to your operating system's trusted certificate store ([macOS](https://support.apple.com/en-gb/guide/keychain-access/kyca2431/mac), [Windows](https://blogs.technet.microsoft.com/sbs/2008/05/08/installing-a-self-signed-certificate-as-a-trusted-root-ca-in-windows-vista/)).
70+
71+
If you are _unable_ to either **obtain a trusted certificate**, or trust the self-signed certificate you can disable certificate verification in Git and GCM.
72+
73+
---
74+
**Security Warning** :warning:
75+
76+
Disabling verification of TLS (SSL) certificates removes protection against a [man-in-the-middle (MITM) attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack).
77+
78+
Only disable certificate verification if you are sure you need to, are aware of all of the risks, and are unable to trust specific self-signed certificates (as described above).
79+
80+
---
81+
82+
The [environment variable `GIT_SSL_NO_VERIFY`](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables#_networking) and [Git configuration option `http.sslVerify`](https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpsslVerify) can be used to control TLS (SSL) certifcate verification.
83+
84+
To disable verification for a specific remote (for example <https://example.com>):
85+
86+
```shell
87+
git config --global http.https://example.com.sslVerify false
88+
```
89+
90+
To disable verification for the current user for **_all remotes_** (**not recommended**):
91+
92+
```shell
93+
# Environment variable (Windows)
94+
SET GIT_SSL_NO_VERIFY=1
95+
96+
# Environment variable (macOS/Linux)
97+
export GIT_SSL_NO_VERIFY=1
98+
99+
# Git configuration (Windows/macOS/Linux)
100+
git config --global http.sslVerify false
101+
```
102+
103+
---
104+
105+
**Note:** You may also experience similar verification errors if you are using a network traffic inspection tool such as [Telerik Fiddler](https://www.telerik.com/fiddler). If you are using such tools please consult their documentation for trusting the proxy root certificates.

src/shared/GitHub/GitHubAuthentication.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private bool TryFindHelperExecutablePath(out string path)
107107
// We currently only have a helper on Windows. If we failed to find the helper we should warn the user.
108108
if (PlatformUtils.IsWindows())
109109
{
110-
Context.StdError.WriteLine($"warning: missing '{helperName}' from installation.");
110+
Context.Streams.Error.WriteLine($"warning: missing '{helperName}' from installation.");
111111
}
112112

113113
return false;

src/shared/Microsoft.Git.CredentialManager.Tests/Commands/EraseCommandTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ public async Task EraseCommand_ExecuteAsync_CallsHostProvider()
4545
providerMock.Setup(x => x.EraseCredentialAsync(It.IsAny<InputArguments>()))
4646
.Returns(Task.CompletedTask);
4747
var providerRegistry = new TestHostProviderRegistry {Provider = providerMock.Object};
48-
var context = new TestCommandContext {StdIn = stdin};
48+
var context = new TestCommandContext
49+
{
50+
Streams = {In = stdin}
51+
};
4952

5053
string[] cmdArgs = {"erase"};
5154
var command = new EraseCommand(providerRegistry);

src/shared/Microsoft.Git.CredentialManager.Tests/Commands/GetCommandTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public async Task GetCommand_ExecuteAsync_CallsHostProviderAndWritesCredential()
6161

6262
await command.ExecuteAsync(context, cmdArgs);
6363

64-
IDictionary<string, string> actualStdOutDict = ParseDictionary(context.StdOut);
64+
IDictionary<string, string> actualStdOutDict = ParseDictionary(context.Streams.Out);
6565

6666
providerMock.Verify(x => x.GetCredentialAsync(It.IsAny<InputArguments>()), Times.Once);
6767
Assert.Equal(expectedStdOutDict, actualStdOutDict);

src/shared/Microsoft.Git.CredentialManager.Tests/Commands/HostProviderCommandBaseTests.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class HostProviderCommandBaseTests
1515
public async Task HostProviderCommandBase_ExecuteAsync_CallsExecuteInternalAsyncWithCorrectArgs()
1616
{
1717
var mockContext = new Mock<ICommandContext>();
18+
var mockStreams = new Mock<IStandardStreams>();
1819
var mockProvider = new Mock<IHostProvider>();
1920
var mockHostRegistry = new Mock<IHostProviderRegistry>();
2021

@@ -28,7 +29,8 @@ public async Task HostProviderCommandBase_ExecuteAsync_CallsExecuteInternalAsync
2829
string standardIn = "protocol=test\nhost=example.com\npath=a/b/c\n\n";
2930
TextReader standardInReader = new StringReader(standardIn);
3031

31-
mockContext.Setup(x => x.StdIn).Returns(standardInReader);
32+
mockStreams.Setup(x => x.In).Returns(standardInReader);
33+
mockContext.Setup(x => x.Streams).Returns(mockStreams.Object);
3234
mockContext.Setup(x => x.Trace).Returns(Mock.Of<ITrace>());
3335
mockContext.Setup(x => x.Settings).Returns(Mock.Of<ISettings>());
3436

@@ -51,6 +53,7 @@ public async Task HostProviderCommandBase_ExecuteAsync_CallsExecuteInternalAsync
5153
public async Task HostProviderCommandBase_ExecuteAsync_ConfiguresSettingsRemoteUri()
5254
{
5355
var mockContext = new Mock<ICommandContext>();
56+
var mockStreams = new Mock<IStandardStreams>();
5457
var mockProvider = new Mock<IHostProvider>();
5558
var mockSettings = new Mock<ISettings>();
5659
var mockHostRegistry = new Mock<IHostProviderRegistry>();
@@ -65,7 +68,8 @@ public async Task HostProviderCommandBase_ExecuteAsync_ConfiguresSettingsRemoteU
6568

6669
mockSettings.SetupProperty(x => x.RemoteUri);
6770

68-
mockContext.Setup(x => x.StdIn).Returns(standardInReader);
71+
mockStreams.Setup(x => x.In).Returns(standardInReader);
72+
mockContext.Setup(x => x.Streams).Returns(mockStreams.Object);
6973
mockContext.Setup(x => x.Trace).Returns(Mock.Of<ITrace>());
7074
mockContext.Setup(x => x.Settings).Returns(mockSettings.Object);
7175

src/shared/Microsoft.Git.CredentialManager.Tests/Commands/StoreCommandTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ public async Task StoreCommand_ExecuteAsync_CallsHostProvider()
5252
providerMock.Setup(x => x.StoreCredentialAsync(It.IsAny<InputArguments>()))
5353
.Returns(Task.CompletedTask);
5454
var providerRegistry = new TestHostProviderRegistry {Provider = providerMock.Object};
55-
var context = new TestCommandContext {StdIn = stdin};
55+
var context = new TestCommandContext
56+
{
57+
Streams = {In = stdin}
58+
};
5659

5760
string[] cmdArgs = {"store"};
5861
var command = new StoreCommand(providerRegistry);

src/shared/Microsoft.Git.CredentialManager.Tests/HttpClientFactoryTests.cs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
3+
using System;
4+
using System.Net;
35
using System.Net.Http;
6+
using Moq;
7+
using Microsoft.Git.CredentialManager.Tests.Objects;
48
using Xunit;
59

610
namespace Microsoft.Git.CredentialManager.Tests
@@ -10,7 +14,7 @@ public class HttpClientFactoryTests
1014
[Fact]
1115
public void HttpClientFactory_GetClient_SetsDefaultHeaders()
1216
{
13-
var factory = new HttpClientFactory();
17+
var factory = new HttpClientFactory(Mock.Of<ITrace>(), Mock.Of<ISettings>(), new TestStandardStreams());
1418

1519
HttpClient client = factory.CreateClient();
1620

@@ -22,12 +26,105 @@ public void HttpClientFactory_GetClient_SetsDefaultHeaders()
2226
[Fact]
2327
public void HttpClientFactory_GetClient_MultipleCalls_ReturnsNewInstance()
2428
{
25-
var factory = new HttpClientFactory();
29+
var factory = new HttpClientFactory(Mock.Of<ITrace>(), Mock.Of<ISettings>(), new TestStandardStreams());
2630

2731
HttpClient client1 = factory.CreateClient();
2832
HttpClient client2 = factory.CreateClient();
2933

3034
Assert.NotSame(client1, client2);
3135
}
36+
37+
[Fact]
38+
public void HttpClientFactory_TryCreateProxy_NoProxy_ReturnsFalseOutNull()
39+
{
40+
const string repoPath = "/tmp/repos/foo";
41+
const string repoRemote = "https://remote.example.com/foo.git";
42+
var repoRemoteUri = new Uri(repoRemote);
43+
44+
var settings = new TestSettings
45+
{
46+
RemoteUri = repoRemoteUri,
47+
RepositoryPath = repoPath
48+
};
49+
var httpFactory = new HttpClientFactory(Mock.Of<ITrace>(), settings, Mock.Of<IStandardStreams>());
50+
51+
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
52+
53+
Assert.False(result);
54+
Assert.Null(proxy);
55+
}
56+
57+
[Fact]
58+
public void HttpClientFactory_TryCreateProxy_ProxyNoCredentials_ReturnsTrueOutProxyWithUrlDefaultCredentials()
59+
{
60+
const string repoPath = "/tmp/repos/foo";
61+
const string repoRemote = "https://remote.example.com/foo.git";
62+
var repoRemoteUri = new Uri(repoRemote);
63+
64+
string proxyConfigString = "https://proxy.example.com/git";
65+
string expectedProxyUrl = proxyConfigString;
66+
67+
var settings = new TestSettings
68+
{
69+
RemoteUri = repoRemoteUri,
70+
RepositoryPath = repoPath,
71+
ProxyConfiguration = new Uri(proxyConfigString)
72+
};
73+
var httpFactory = new HttpClientFactory(Mock.Of<ITrace>(), settings, Mock.Of<IStandardStreams>());
74+
75+
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
76+
77+
Assert.True(result);
78+
Assert.NotNull(proxy);
79+
var configuredProxyUrl = proxy.GetProxy(repoRemoteUri);
80+
Assert.Equal(expectedProxyUrl, configuredProxyUrl.ToString());
81+
82+
AssertDefaultCredentials(proxy.Credentials);
83+
}
84+
85+
[Fact]
86+
public void HttpClientFactory_TryCreateProxy_ProxyWithCredentials_ReturnsTrueOutProxyWithUrlConfiguredCredentials()
87+
{
88+
const string proxyScheme = "https";
89+
const string proxyUser = "john.doe";
90+
const string proxyPass = "letmein";
91+
const string proxyHost = "proxy.example.com/git";
92+
const string repoPath = "/tmp/repos/foo";
93+
const string repoRemote = "https://remote.example.com/foo.git";
94+
95+
string proxyConfigString = $"{proxyScheme}://{proxyUser}:{proxyPass}@{proxyHost}";
96+
string expectedProxyUrl = $"{proxyScheme}://{proxyHost}";
97+
var repoRemoteUri = new Uri(repoRemote);
98+
99+
var settings = new TestSettings
100+
{
101+
RemoteUri = repoRemoteUri,
102+
RepositoryPath = repoPath,
103+
ProxyConfiguration = new Uri(proxyConfigString)
104+
};
105+
var httpFactory = new HttpClientFactory(Mock.Of<ITrace>(), settings, Mock.Of<IStandardStreams>());
106+
107+
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
108+
109+
Assert.True(result);
110+
Assert.NotNull(proxy);
111+
var configuredProxyUrl = proxy.GetProxy(repoRemoteUri);
112+
Assert.Equal(expectedProxyUrl, configuredProxyUrl.ToString());
113+
114+
Assert.NotNull(proxy.Credentials);
115+
Assert.IsType<NetworkCredential>(proxy.Credentials);
116+
var configuredCredentials = (NetworkCredential) proxy.Credentials;
117+
Assert.Equal(proxyUser, configuredCredentials.UserName);
118+
Assert.Equal(proxyPass, configuredCredentials.Password);
119+
}
120+
121+
private static void AssertDefaultCredentials(ICredentials credentials)
122+
{
123+
var netCred = (NetworkCredential) credentials;
124+
125+
Assert.Equal(string.Empty, netCred.Domain);
126+
Assert.Equal(string.Empty, netCred.UserName);
127+
Assert.Equal(string.Empty, netCred.Password);
128+
}
32129
}
33130
}

0 commit comments

Comments
 (0)