Skip to content

Commit b32e90c

Browse files
authored
Merge pull request #22477 from michaelnebel/csharp/refactordependabotproxy
C#: Re-factor DependabotProxy class to allow unit-testing.
2 parents b5d570f + 2dc8ef9 commit b32e90c

5 files changed

Lines changed: 272 additions & 56 deletions

File tree

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs

Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -18,84 +18,54 @@ public class DependabotProxy : IDependabotProxy
1818
/// <param name="URL">The URL of the package registry.</param>
1919
public record class RegistryConfig(string Type, string URL);
2020

21-
private readonly string host;
22-
private readonly string port;
23-
2421
public string Address { get; }
2522

26-
public HashSet<string> RegistryURLs { get; }
23+
public HashSet<string> RegistryURLs { get; } = [];
2724

2825
public string? CertificatePath { get; private set; }
2926

3027
public X509Certificate2? Certificate { get; private set; }
3128

32-
internal static IDependabotProxy? GetDependabotProxy(
33-
ILogger logger, IDiagnosticsWriter diagnosticsWriter, TemporaryDirectory tempWorkingDirectory)
29+
private DependabotProxy(IDependabotProxyConfiguration config, ILogger logger, TemporaryDirectory tempWorkingDirectory)
3430
{
35-
// Setting HTTP(S)_PROXY and SSL_CERT_FILE have no effect on Windows or macOS,
36-
// but we would still end up using the Dependabot proxy to check for feed reachability.
37-
// This would result in us discovering that the feeds are reachable, but `dotnet` would
38-
// fail to connect to them. To prevent this from happening, we do not initialise an
39-
// instance of `DependabotProxy` on those platforms.
40-
if (SystemBuildActions.Instance.IsWindows() || SystemBuildActions.Instance.IsMacOs()) return null;
41-
42-
// Obtain and store the address of the Dependabot proxy, if available.
43-
var host = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyHost);
44-
var port = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyPort);
45-
46-
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(port))
47-
{
48-
logger.LogInfo("No Dependabot proxy credentials are configured.");
49-
return null;
50-
}
51-
52-
var result = new DependabotProxy(host, port);
53-
logger.LogInfo($"Dependabot proxy configured at {result.Address}");
31+
Address = $"http://{config.Host}:{config.Port}";
5432

55-
// Obtain and store the proxy's certificate, if available.
56-
var cert = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyCertificate);
57-
58-
if (!string.IsNullOrWhiteSpace(cert))
33+
if (!string.IsNullOrWhiteSpace(config.Certificate))
5934
{
6035
var certDirPath = new DirectoryInfo(Path.Join(tempWorkingDirectory.DirInfo.FullName, ".dependabot-proxy"));
6136
Directory.CreateDirectory(certDirPath.FullName);
6237

63-
result.CertificatePath = Path.Join(certDirPath.FullName, "proxy.crt");
64-
var certFile = new FileInfo(result.CertificatePath);
38+
CertificatePath = Path.Join(certDirPath.FullName, "proxy.crt");
39+
var certFile = new FileInfo(CertificatePath);
6540

6641
using var writer = certFile.CreateText();
67-
writer.Write(cert);
42+
writer.Write(config.Certificate);
6843
writer.Close();
6944

70-
logger.LogInfo($"Stored Dependabot proxy certificate at {result.CertificatePath}");
45+
logger.LogInfo($"Stored Dependabot proxy certificate at {CertificatePath}");
7146

72-
result.Certificate = X509Certificate2.CreateFromPem(cert);
47+
Certificate = X509Certificate2.CreateFromPem(config.Certificate);
7348
}
7449

75-
// Try to obtain the list of private registry URLs.
76-
var registryURLs = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyURLs);
77-
78-
if (!string.IsNullOrWhiteSpace(registryURLs))
50+
if (!string.IsNullOrWhiteSpace(config.RegistryURLs))
7951
{
8052
try
8153
{
82-
// The value of the environment variable should be a JSON array of objects, such as:
83-
// [ { "type": "nuget_feed", "url": "https://nuget.pkg.github.com/org/index.json" } ]
84-
var array = JsonConvert.DeserializeObject<List<RegistryConfig>>(registryURLs);
54+
var array = JsonConvert.DeserializeObject<List<RegistryConfig>>(config.RegistryURLs);
8555
if (array is not null)
8656
{
87-
foreach (RegistryConfig config in array)
57+
foreach (RegistryConfig registry in array)
8858
{
8959
// The array contains all configured private registries, not just ones for C#.
9060
// We ignore the non-C# ones here.
91-
if (!config.Type.Equals("nuget_feed"))
61+
if (!registry.Type.Equals("nuget_feed"))
9262
{
93-
logger.LogDebug($"Ignoring registry at '{config.URL}' since it is not of type 'nuget_feed'.");
63+
logger.LogDebug($"Ignoring registry at '{registry.URL}' since it is not of type 'nuget_feed'.");
9464
continue;
9565
}
9666

97-
logger.LogInfo($"Found private registry at '{config.URL}'");
98-
result.RegistryURLs.Add(config.URL);
67+
logger.LogInfo($"Found private registry at '{registry.URL}'");
68+
RegistryURLs.Add(registry.URL);
9969
}
10070
}
10171
}
@@ -104,6 +74,39 @@ public record class RegistryConfig(string Type, string URL);
10474
logger.LogError($"Unable to parse '{EnvironmentVariableNames.ProxyURLs}': {ex.Message}");
10575
}
10676
}
77+
}
78+
79+
internal static IDependabotProxy? Make(ILogger logger, IDiagnosticsWriter diagnosticsWriter, TemporaryDirectory tempWorkingDirectory)
80+
{
81+
// Setting HTTP(S)_PROXY and SSL_CERT_FILE have no effect on Windows or macOS,
82+
// but we would still end up using the Dependabot proxy to check for feed reachability.
83+
// This would result in us discovering that the feeds are reachable, but `dotnet` would
84+
// fail to connect to them. To prevent this from happening, we do not initialise an
85+
// instance of `DependabotProxy` on those platforms.
86+
if (SystemBuildActions.Instance.IsWindows() || SystemBuildActions.Instance.IsMacOs())
87+
{
88+
return null;
89+
}
90+
91+
return Make(new DependabotProxyConfiguration(), logger, diagnosticsWriter, tempWorkingDirectory);
92+
}
93+
94+
/// <summary>
95+
/// Creates an instance of the Dependabot proxy using the specified configuration.
96+
/// Returns null if the proxy cannot be created.
97+
/// This overload is exposed primarily to enable platform-independent unit testing.
98+
/// </summary>
99+
internal static IDependabotProxy? Make(
100+
IDependabotProxyConfiguration proxyConfig, ILogger logger, IDiagnosticsWriter diagnosticsWriter, TemporaryDirectory tempWorkingDirectory)
101+
{
102+
if (string.IsNullOrWhiteSpace(proxyConfig.Host) || string.IsNullOrWhiteSpace(proxyConfig.Port))
103+
{
104+
logger.LogDebug("No Dependabot proxy credentials are configured.");
105+
return null;
106+
}
107+
108+
var result = new DependabotProxy(proxyConfig, logger, tempWorkingDirectory);
109+
logger.LogInfo($"Dependabot proxy configured at {result.Address}");
107110

108111
// Emit a diagnostic for the discovered private registries, so that it is easy
109112
// for users to see that they were picked up.
@@ -125,17 +128,9 @@ public record class RegistryConfig(string Type, string URL);
125128
return result;
126129
}
127130

128-
private DependabotProxy(string host, string port)
129-
{
130-
this.host = host;
131-
this.port = port;
132-
this.Address = $"http://{this.host}:{this.port}";
133-
this.RegistryURLs = new HashSet<string>();
134-
}
135-
136131
public void Dispose()
137132
{
138-
this.Certificate?.Dispose();
133+
Certificate?.Dispose();
139134
}
140135
}
141136
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace Semmle.Extraction.CSharp.DependencyFetching
4+
{
5+
public class DependabotProxyConfiguration : IDependabotProxyConfiguration
6+
{
7+
public string? Host { get; } = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyHost);
8+
9+
public string? Port { get; } = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyPort);
10+
11+
public string? Certificate { get; } = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyCertificate);
12+
13+
public string? RegistryURLs { get; } = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyURLs);
14+
}
15+
}

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ void exitCallback(int ret, string msg, bool silent)
106106
return BuildScript.Success;
107107
}).Run(SystemBuildActions.Instance, startCallback, exitCallback);
108108

109-
dependabotProxy = DependabotProxy.GetDependabotProxy(logger, diagnosticsWriter, tempWorkingDirectory);
109+
dependabotProxy = DependabotProxy.Make(logger, diagnosticsWriter, tempWorkingDirectory);
110110

111111
try
112112
{
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
3+
namespace Semmle.Extraction.CSharp.DependencyFetching
4+
{
5+
public interface IDependabotProxyConfiguration
6+
{
7+
// The host of the Dependabot proxy, if available.
8+
string? Host { get; }
9+
10+
// The port of the Dependabot proxy, if available.
11+
string? Port { get; }
12+
13+
// The certificate of the Dependabot proxy, if available.
14+
string? Certificate { get; }
15+
16+
// The list of package registries that are configured for the proxy, if any.
17+
// The value of the environment variable should be a JSON array of objects, such as:
18+
// [ { "type": "nuget_feed", "url": "https://nuget.pkg.github.com/org/index.json" } ]
19+
string? RegistryURLs { get; }
20+
}
21+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using Xunit;
2+
using System;
3+
using System.IO;
4+
using Semmle.Extraction.CSharp.DependencyFetching;
5+
using Semmle.Util;
6+
7+
namespace Semmle.Extraction.Tests
8+
{
9+
public class DependabotConfigurationStub : IDependabotProxyConfiguration
10+
{
11+
public string? Host { get; set; }
12+
public string? Port { get; set; }
13+
public string? Certificate { get; set; }
14+
public string? RegistryURLs { get; set; }
15+
}
16+
17+
public class DiagnosticsWriterStub : IDiagnosticsWriter
18+
{
19+
public void AddEntry(Semmle.Util.DiagnosticMessage entry) { }
20+
public void Dispose() { }
21+
}
22+
23+
public class DependabotProxyTests
24+
{
25+
private static TemporaryDirectory MakeTemporaryDirectory()
26+
{
27+
var tmp = Path.Join(Path.GetTempPath(), "DependabotProxyTests", Guid.NewGuid().ToString());
28+
return new TemporaryDirectory(tmp, "testing", new LoggerStub());
29+
}
30+
31+
[Fact]
32+
public void TestDependabotProxyCreation1()
33+
{
34+
// Setup
35+
var config = new DependabotConfigurationStub
36+
{
37+
Host = "localhost",
38+
Port = "",
39+
};
40+
41+
// Execute
42+
using var tempWorkingDirectory = MakeTemporaryDirectory();
43+
using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory);
44+
45+
// Verify
46+
Assert.Null(proxy);
47+
}
48+
49+
[Fact]
50+
public void TestDependabotProxyCreation2()
51+
{
52+
// Setup
53+
var config = new DependabotConfigurationStub
54+
{
55+
Port = "8080",
56+
};
57+
58+
// Execute
59+
using var tempWorkingDirectory = MakeTemporaryDirectory();
60+
using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory);
61+
62+
// Verify
63+
Assert.Null(proxy);
64+
}
65+
66+
private const string ExampleCertificate = """
67+
-----BEGIN CERTIFICATE-----
68+
MIIFJTCCAw2gAwIBAgIUDImU6YnuAqJ1QuRp+OpJQPnPu6wwDQYJKoZIhvcNAQEL
69+
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDkwMTEyMjUzMVoXDTI3MDkw
70+
MTEyMjUzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF
71+
AAOCAg8AMIICCgKCAgEAnlp7yQ1VuocMwIZWlCle3bEM86+1ED6BFfPFpIrRhfUT
72+
c+5IvPng8TIZPO4mROp5G9YDZfOtXW2bwktyZNUhsBcxqUT1lmXit21vc5W9Gxx5
73+
4G8nyF4/FcjFkmxkZifxiUCdBceDcE7+kx2itq/a7gLPlyTzvz5etu1nHEC3Jg/y
74+
TVhAwdwysgAo9WymFCczDa2ga6nOPBOaxwLnoPl9041KSu5oIo9QC0Im+US1R18Q
75+
/mXa+wkmjf+bYAkE/pZie8z8Q7h9yppTngGzkoDEebFYyaMr8MXlFdWS8f/eMwSp
76+
iMFSsmlCqgUbA672APxzOcuSMMYrblzGkvZp23qbNjwQuQKlgAYBTSGltLv4U8JF
77+
ePNcgDCY6RG55rNvF1gk1L2h25jcw1LX6fSvQGCOkzNmP03AhqZBUigO1Zt0zLwi
78+
K4m0bH7nPLJFEN6tI3tybyZeC2RVyiSHvOkgx35Qj8RQ3XMVkImJNBYOMc2MkmMZ
79+
ux6XMiHqXCON4zaWuWSovciZeMAQAspCrzVDLH6p2DWEfw/zDfQNU3iLk21sZGei
80+
0GKzs8zrxUcqOU9V4Cnm+7JJ6eqS72f1+wX0ROb3djC6KgCE/NaHqo4apiI3K+CH
81+
T0rVRsJIHyT39YO1c1I1vhAKRSH5kQVe3qRfIT/AuaDLQY6WqGPzrOkem78sjtsC
82+
AwEAAaNvMG0wHQYDVR0OBBYEFK0DP5MD6mhEcdcm346uwoPL2NFGMB8GA1UdIwQY
83+
MBaAFK0DP5MD6mhEcdcm346uwoPL2NFGMA8GA1UdEwEB/wQFMAMBAf8wGgYDVR0R
84+
BBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQCc5u8qNHHG
85+
kONjfvq7Denq6QaEt4dZZDDODAvgUzZnnBjEhgrp7zfxtbyU/I0+DWnKQMKA9wPM
86+
ktiFGd0lldEqoT+E7b0kN124lBGqZ/uYkhsWZ0Nc5dD+UB9oJszwOc5KNuquOnr6
87+
SbsfXVm4yLvVLXl67c0jvqvRgGg9/6Q6eMzohW6abMdbYhS28/DsJhCea/dV3+L1
88+
oVJ3O/A8e86m174ZCGE8s9UtnVYylBkAryDqaaQLdOBQ2C7uxdRAUNHSIa2JlqUc
89+
5+cod8lFojKb74hbgj6wkXyajsFttqYMh7CeASsnjZXDQ4MC3DqqDVCZuNvJ85Rt
90+
ya3Tljp4Ln2AAAoKC3REUeU8PQqpk1vVIj0FSr3RvBTvwzyNfWFVqyBiXTATuV9n
91+
6AemqqXo5MZrHHeRaSTF8A70Jxbt9yx75xQxp3O3tdEL1Mxbl9X7c/hizOfLbeHH
92+
IkAgzALQgi87Zbf2tOhRwH5NrB4ijyUUfovRHUwzsZOoTNqlVeNzbDRVbegx9V99
93+
/3vwNZgpStGl/JYhN9qY5hJKnC64ltMvuNGpLeJCGyFkrtFS8gKkgR7VKrGo7h3+
94+
Zo8rz8TFjP7RmSgQbrmFuPqNOGXzPidu2sMMFacKV7Rn4bEtHzW3MDhqVD4w/pGD
95+
L0xpnWjzLYltVjz8mo07yh+zQ10G71Cl1w==
96+
-----END CERTIFICATE-----
97+
""";
98+
99+
[Fact]
100+
public void TestDependabotProxyCertificate()
101+
{
102+
// Setup
103+
var config = new DependabotConfigurationStub
104+
{
105+
Port = "8080",
106+
Host = "localhost",
107+
Certificate = ExampleCertificate
108+
};
109+
110+
// Execute
111+
using var tempWorkingDirectory = MakeTemporaryDirectory();
112+
using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory);
113+
114+
// Verify
115+
Assert.NotNull(proxy);
116+
Assert.Equal("http://localhost:8080", proxy.Address);
117+
Assert.NotNull(proxy.Certificate);
118+
Assert.NotNull(proxy.CertificatePath);
119+
}
120+
121+
[Fact]
122+
public void TestDependabotRegistryUrls1()
123+
{
124+
// Setup
125+
var config = new DependabotConfigurationStub
126+
{
127+
Port = "8080",
128+
Host = "localhost",
129+
RegistryURLs = "Doesn't parse as a JSON list"
130+
};
131+
132+
// Execute
133+
using var tempWorkingDirectory = MakeTemporaryDirectory();
134+
using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory);
135+
136+
// Verify
137+
Assert.NotNull(proxy);
138+
Assert.Equal([], proxy.RegistryURLs);
139+
}
140+
141+
[Fact]
142+
public void TestDependabotRegistryUrls2()
143+
{
144+
// Setup
145+
var config = new DependabotConfigurationStub
146+
{
147+
Port = "8080",
148+
Host = "localhost",
149+
RegistryURLs = "[ { \"type\": \"nuget_feed\", \"url\": \"https://nuget.pkg.github.com/org/index.json\" } ]"
150+
};
151+
152+
// Execute
153+
using var tempWorkingDirectory = MakeTemporaryDirectory();
154+
using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory);
155+
156+
// Verify
157+
Assert.NotNull(proxy);
158+
Assert.Equal([
159+
"https://nuget.pkg.github.com/org/index.json"
160+
], proxy.RegistryURLs);
161+
}
162+
163+
[Fact]
164+
public void TestDependabotRegistryUrls3()
165+
{
166+
// Setup
167+
var config = new DependabotConfigurationStub
168+
{
169+
Port = "8080",
170+
Host = "localhost",
171+
RegistryURLs = "[ { \"type\": \"nuget_feed\", \"url\": \"https://example.com/org/index.json\" }, { \"type\": \"wrong_type\", \"url\": \"https://nuget.pkg.github.com/org/index.json\" } ]"
172+
};
173+
174+
// Execute
175+
using var tempWorkingDirectory = MakeTemporaryDirectory();
176+
using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory);
177+
178+
// Verify
179+
Assert.NotNull(proxy);
180+
Assert.Equal([
181+
"https://example.com/org/index.json"
182+
], proxy.RegistryURLs);
183+
}
184+
}
185+
}

0 commit comments

Comments
 (0)