Skip to content

Commit 1232c7f

Browse files
Remove Dev Proxy CA certificate when uninstalling for Win (#1208)
* Add CLI 'remove' command to remove Dev Proxy certificate * Update win-installers to remove certificate when uninstalling * Fix beta executable name * Adjust certificate removal identifier of installer * fix: Add prompt confirmation to remove certificates and --force option to bypass * Update installers to remove cert silently * Remove trusted certificate on Mac * Fix project updating resources only if newer one * Add HasRunFlag class and refactor IsFirstRun() * Add .hasrun flag removal along with certificates removal (#1241) * Hide possible exceptions during .hasrun flag deletion (#1241) * Remove redundant -Flag suffix * Rename defaultValue to acceptByDefault parameter * Use 'var' instead of explicit type * Rename IsFirstRun to CreateIfMissing function * Move HasRunFlag to Abstractions.Utils * Minor fixes --------- Co-authored-by: Waldek Mastykarz <[email protected]>
1 parent 387c2fc commit 1232c7f

File tree

7 files changed

+174
-20
lines changed

7 files changed

+174
-20
lines changed

DevProxy/Commands/CertCommand.cs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using DevProxy.Abstractions.Utils;
56
using DevProxy.Proxy;
67
using System.CommandLine;
8+
using System.CommandLine.Invocation;
79
using System.CommandLine.Parsing;
10+
using System.Diagnostics;
11+
using Titanium.Web.Proxy.Helpers;
812

913
namespace DevProxy.Commands;
1014

1115
sealed class CertCommand : Command
1216
{
1317
private readonly ILogger _logger;
18+
private readonly Option<bool> _forceOption = new(["--force", "-f"], "Don't prompt for confirmation when removing the certificate");
1419

1520
public CertCommand(ILogger<CertCommand> logger) :
1621
base("cert", "Manage the Dev Proxy certificate")
@@ -25,9 +30,14 @@ private void ConfigureCommand()
2530
var certEnsureCommand = new Command("ensure", "Ensure certificates are setup (creates root if required). Also makes root certificate trusted.");
2631
certEnsureCommand.SetHandler(EnsureCertAsync);
2732

33+
var certRemoveCommand = new Command("remove", "Remove the certificate from Root Store");
34+
certRemoveCommand.SetHandler(RemoveCert);
35+
certRemoveCommand.AddOptions(new[] { _forceOption }.OrderByName());
36+
2837
this.AddCommands(new List<Command>
2938
{
30-
certEnsureCommand
39+
certEnsureCommand,
40+
certRemoveCommand,
3141
}.OrderByName());
3242
}
3343

@@ -48,4 +58,82 @@ private async Task EnsureCertAsync()
4858

4959
_logger.LogTrace("EnsureCertAsync() finished");
5060
}
61+
62+
public void RemoveCert(InvocationContext invocationContext)
63+
{
64+
_logger.LogTrace("RemoveCert() called");
65+
66+
try
67+
{
68+
var isForced = invocationContext.ParseResult.GetValueForOption(_forceOption);
69+
if (!isForced)
70+
{
71+
var isConfirmed = PromptConfirmation("Do you want to remove the root certificate", acceptByDefault: false);
72+
if (!isConfirmed)
73+
{
74+
return;
75+
}
76+
}
77+
78+
_logger.LogInformation("Uninstalling the root certificate...");
79+
80+
RemoveTrustedCertificateOnMac();
81+
ProxyEngine.ProxyServer.CertificateManager.RemoveTrustedRootCertificate(machineTrusted: false);
82+
83+
_logger.LogInformation("DONE");
84+
}
85+
catch (Exception ex)
86+
{
87+
_logger.LogError(ex, "Error removing certificate");
88+
}
89+
finally
90+
{
91+
_logger.LogTrace("RemoveCert() finished");
92+
}
93+
}
94+
95+
private static bool PromptConfirmation(string message, bool acceptByDefault)
96+
{
97+
while (true)
98+
{
99+
Console.Write(message + $" ({(acceptByDefault ? "Y/n" : "y/N")}): ");
100+
var answer = Console.ReadLine();
101+
102+
if (string.IsNullOrWhiteSpace(answer))
103+
{
104+
return acceptByDefault;
105+
}
106+
else if (string.Equals("y", answer, StringComparison.OrdinalIgnoreCase))
107+
{
108+
return true;
109+
}
110+
else if (string.Equals("n", answer, StringComparison.OrdinalIgnoreCase))
111+
{
112+
return false;
113+
}
114+
}
115+
}
116+
117+
private static void RemoveTrustedCertificateOnMac()
118+
{
119+
if (!RunTime.IsMac)
120+
{
121+
return;
122+
}
123+
124+
var bashScriptPath = Path.Join(ProxyUtils.AppFolder, "remove-cert.sh");
125+
var startInfo = new ProcessStartInfo()
126+
{
127+
FileName = "/bin/bash",
128+
Arguments = bashScriptPath,
129+
UseShellExecute = false,
130+
CreateNoWindow = true,
131+
};
132+
133+
using var process = new Process() { StartInfo = startInfo };
134+
_ = process.Start();
135+
process.WaitForExit();
136+
137+
HasRunFlag.Remove();
138+
}
51139
}

DevProxy/DevProxy.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,11 @@
6464
<None Update="devproxy-errors.json">
6565
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6666
</None>
67+
<None Update="remove-cert.sh">
68+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
69+
</None>
6770
<None Update="toggle-proxy.sh">
68-
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
71+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
6972
</None>
7073
<None Update="trust-cert.sh">
7174
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>

DevProxy/HasRunFlag.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using DevProxy.Abstractions.Utils;
6+
7+
namespace DevProxy;
8+
9+
static class HasRunFlag
10+
{
11+
private static readonly string filename = Path.Combine(ProxyUtils.AppFolder!, ".hasrun");
12+
13+
public static bool CreateIfMissing()
14+
{
15+
if (File.Exists(filename))
16+
{
17+
return false;
18+
}
19+
20+
return Create();
21+
}
22+
23+
private static bool Create()
24+
{
25+
try
26+
{
27+
File.WriteAllText(filename, "");
28+
}
29+
catch
30+
{
31+
return false;
32+
}
33+
return true;
34+
}
35+
36+
public static void Remove()
37+
{
38+
try
39+
{
40+
if (File.Exists(filename))
41+
{
42+
File.Delete(filename);
43+
}
44+
}
45+
catch { }
46+
}
47+
}

DevProxy/Proxy/ProxyEngine.cs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private void FirstRunSetup()
179179
{
180180
if (!RunTime.IsMac ||
181181
_config.NoFirstRun ||
182-
!IsFirstRun() ||
182+
!HasRunFlag.CreateIfMissing() ||
183183
!_config.InstallCert)
184184
{
185185
return;
@@ -615,23 +615,6 @@ private static void ToggleSystemProxy(ToggleSystemProxyAction toggle, string? ip
615615
process.WaitForExit();
616616
}
617617

618-
private static bool IsFirstRun()
619-
{
620-
var firstRunFilePath = Path.Combine(ProxyUtils.AppFolder!, ".hasrun");
621-
if (File.Exists(firstRunFilePath))
622-
{
623-
return false;
624-
}
625-
626-
try
627-
{
628-
File.WriteAllText(firstRunFilePath, "");
629-
}
630-
catch { }
631-
632-
return true;
633-
}
634-
635618
private static int GetProcessId(TunnelConnectSessionEventArgs e)
636619
{
637620
if (RunTime.IsWindows)

DevProxy/remove-cert.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
set -e
3+
4+
if [ "$(uname -s)" != "Darwin" ]; then
5+
echo "Error: this shell script should be run on macOS."
6+
exit 1
7+
fi
8+
9+
echo -e "\nRemove the self-signed certificate from your Keychain."
10+
11+
cert_name="Dev Proxy CA"
12+
cert_filename="dev-proxy-ca.pem"
13+
14+
# export cert from keychain to PEM
15+
echo "Exporting '$cert_name' certificate..."
16+
security find-certificate -c "$cert_name" -a -p > "$cert_filename"
17+
18+
# add trusted cert to keychain
19+
echo "Removing Dev Proxy trust settings..."
20+
security remove-trusted-cert "$cert_filename"
21+
22+
# remove exported cert
23+
echo "Cleaning up..."
24+
rm "$cert_filename"
25+
echo -e "\033[0;32mDONE\033[0m\n"

install-beta.iss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#define MyAppVersion "0.28.0-beta.1"
88
#define MyAppPublisher ".NET Foundation"
99
#define MyAppURL "https://aka.ms/devproxy"
10+
#define DevProxyExecutable "devproxy-beta.exe"
1011

1112
[Setup]
1213
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
@@ -45,6 +46,9 @@ Source: ".\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsu
4546
[UninstallDelete]
4647
Type:files;Name:"{app}\rootCert.pfx"
4748

49+
[UninstallRun]
50+
Filename: "{app}\{#DevProxyExecutable}"; Parameters: "cert remove --force"; RunOnceId: "RemoveCert"; Flags: runhidden;
51+
4852
[Code]
4953
procedure RemovePath(Path: string);
5054
var

install.iss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#define MyAppVersion "0.28.0"
88
#define MyAppPublisher ".NET Foundation"
99
#define MyAppURL "https://aka.ms/devproxy"
10+
#define DevProxyExecutable "devproxy.exe"
1011

1112
[Setup]
1213
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
@@ -45,6 +46,9 @@ Source: ".\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsu
4546
[UninstallDelete]
4647
Type:files;Name:"{app}\rootCert.pfx"
4748

49+
[UninstallRun]
50+
Filename: "{app}\{#DevProxyExecutable}"; Parameters: "cert remove --force"; RunOnceId: "RemoveCert"; Flags: runhidden;
51+
4852
[Code]
4953
procedure RemovePath(Path: string);
5054
var

0 commit comments

Comments
 (0)