diff --git a/DevProxy/Commands/CertCommand.cs b/DevProxy/Commands/CertCommand.cs index ac56f91b..fe4607bf 100644 --- a/DevProxy/Commands/CertCommand.cs +++ b/DevProxy/Commands/CertCommand.cs @@ -2,15 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using DevProxy.Abstractions.Utils; using DevProxy.Proxy; using System.CommandLine; +using System.CommandLine.Invocation; using System.CommandLine.Parsing; +using System.Diagnostics; +using Titanium.Web.Proxy.Helpers; namespace DevProxy.Commands; sealed class CertCommand : Command { private readonly ILogger _logger; + private readonly Option _forceOption = new(["--force", "-f"], "Don't prompt for confirmation when removing the certificate"); public CertCommand(ILogger logger) : base("cert", "Manage the Dev Proxy certificate") @@ -25,9 +30,14 @@ private void ConfigureCommand() var certEnsureCommand = new Command("ensure", "Ensure certificates are setup (creates root if required). Also makes root certificate trusted."); certEnsureCommand.SetHandler(EnsureCertAsync); + var certRemoveCommand = new Command("remove", "Remove the certificate from Root Store"); + certRemoveCommand.SetHandler(RemoveCert); + certRemoveCommand.AddOptions(new[] { _forceOption }.OrderByName()); + this.AddCommands(new List { - certEnsureCommand + certEnsureCommand, + certRemoveCommand, }.OrderByName()); } @@ -48,4 +58,82 @@ private async Task EnsureCertAsync() _logger.LogTrace("EnsureCertAsync() finished"); } + + public void RemoveCert(InvocationContext invocationContext) + { + _logger.LogTrace("RemoveCert() called"); + + try + { + var isForced = invocationContext.ParseResult.GetValueForOption(_forceOption); + if (!isForced) + { + var isConfirmed = PromptConfirmation("Do you want to remove the root certificate", acceptByDefault: false); + if (!isConfirmed) + { + return; + } + } + + _logger.LogInformation("Uninstalling the root certificate..."); + + RemoveTrustedCertificateOnMac(); + ProxyEngine.ProxyServer.CertificateManager.RemoveTrustedRootCertificate(machineTrusted: false); + + _logger.LogInformation("DONE"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error removing certificate"); + } + finally + { + _logger.LogTrace("RemoveCert() finished"); + } + } + + private static bool PromptConfirmation(string message, bool acceptByDefault) + { + while (true) + { + Console.Write(message + $" ({(acceptByDefault ? "Y/n" : "y/N")}): "); + var answer = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(answer)) + { + return acceptByDefault; + } + else if (string.Equals("y", answer, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + else if (string.Equals("n", answer, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + + private static void RemoveTrustedCertificateOnMac() + { + if (!RunTime.IsMac) + { + return; + } + + var bashScriptPath = Path.Join(ProxyUtils.AppFolder, "remove-cert.sh"); + var startInfo = new ProcessStartInfo() + { + FileName = "/bin/bash", + Arguments = bashScriptPath, + UseShellExecute = false, + CreateNoWindow = true, + }; + + using var process = new Process() { StartInfo = startInfo }; + _ = process.Start(); + process.WaitForExit(); + + HasRunFlag.Remove(); + } } \ No newline at end of file diff --git a/DevProxy/DevProxy.csproj b/DevProxy/DevProxy.csproj index 578efe20..d5347577 100644 --- a/DevProxy/DevProxy.csproj +++ b/DevProxy/DevProxy.csproj @@ -64,8 +64,11 @@ PreserveNewest + + PreserveNewest + - Always + PreserveNewest PreserveNewest diff --git a/DevProxy/HasRunFlag.cs b/DevProxy/HasRunFlag.cs new file mode 100644 index 00000000..3ad9afe1 --- /dev/null +++ b/DevProxy/HasRunFlag.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using DevProxy.Abstractions.Utils; + +namespace DevProxy; + +static class HasRunFlag +{ + private static readonly string filename = Path.Combine(ProxyUtils.AppFolder!, ".hasrun"); + + public static bool CreateIfMissing() + { + if (File.Exists(filename)) + { + return false; + } + + return Create(); + } + + private static bool Create() + { + try + { + File.WriteAllText(filename, ""); + } + catch + { + return false; + } + return true; + } + + public static void Remove() + { + try + { + if (File.Exists(filename)) + { + File.Delete(filename); + } + } + catch { } + } +} diff --git a/DevProxy/Proxy/ProxyEngine.cs b/DevProxy/Proxy/ProxyEngine.cs index 314e454d..55949f37 100755 --- a/DevProxy/Proxy/ProxyEngine.cs +++ b/DevProxy/Proxy/ProxyEngine.cs @@ -179,7 +179,7 @@ private void FirstRunSetup() { if (!RunTime.IsMac || _config.NoFirstRun || - !IsFirstRun() || + !HasRunFlag.CreateIfMissing() || !_config.InstallCert) { return; @@ -615,23 +615,6 @@ private static void ToggleSystemProxy(ToggleSystemProxyAction toggle, string? ip process.WaitForExit(); } - private static bool IsFirstRun() - { - var firstRunFilePath = Path.Combine(ProxyUtils.AppFolder!, ".hasrun"); - if (File.Exists(firstRunFilePath)) - { - return false; - } - - try - { - File.WriteAllText(firstRunFilePath, ""); - } - catch { } - - return true; - } - private static int GetProcessId(TunnelConnectSessionEventArgs e) { if (RunTime.IsWindows) diff --git a/DevProxy/remove-cert.sh b/DevProxy/remove-cert.sh new file mode 100644 index 00000000..67ccc24c --- /dev/null +++ b/DevProxy/remove-cert.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +if [ "$(uname -s)" != "Darwin" ]; then + echo "Error: this shell script should be run on macOS." + exit 1 +fi + +echo -e "\nRemove the self-signed certificate from your Keychain." + +cert_name="Dev Proxy CA" +cert_filename="dev-proxy-ca.pem" + +# export cert from keychain to PEM +echo "Exporting '$cert_name' certificate..." +security find-certificate -c "$cert_name" -a -p > "$cert_filename" + +# add trusted cert to keychain +echo "Removing Dev Proxy trust settings..." +security remove-trusted-cert "$cert_filename" + +# remove exported cert +echo "Cleaning up..." +rm "$cert_filename" +echo -e "\033[0;32mDONE\033[0m\n" \ No newline at end of file diff --git a/install-beta.iss b/install-beta.iss index 09d58b0c..831729b6 100644 --- a/install-beta.iss +++ b/install-beta.iss @@ -7,6 +7,7 @@ #define MyAppVersion "0.28.0-beta.1" #define MyAppPublisher ".NET Foundation" #define MyAppURL "https://aka.ms/devproxy" +#define DevProxyExecutable "devproxy-beta.exe" [Setup] ; 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 [UninstallDelete] Type:files;Name:"{app}\rootCert.pfx" +[UninstallRun] +Filename: "{app}\{#DevProxyExecutable}"; Parameters: "cert remove --force"; RunOnceId: "RemoveCert"; Flags: runhidden; + [Code] procedure RemovePath(Path: string); var diff --git a/install.iss b/install.iss index 20fa9c78..437ba969 100644 --- a/install.iss +++ b/install.iss @@ -7,6 +7,7 @@ #define MyAppVersion "0.28.0" #define MyAppPublisher ".NET Foundation" #define MyAppURL "https://aka.ms/devproxy" +#define DevProxyExecutable "devproxy.exe" [Setup] ; 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 [UninstallDelete] Type:files;Name:"{app}\rootCert.pfx" +[UninstallRun] +Filename: "{app}\{#DevProxyExecutable}"; Parameters: "cert remove --force"; RunOnceId: "RemoveCert"; Flags: runhidden; + [Code] procedure RemovePath(Path: string); var