Updating Installer

- UI for providing a cookie at install time

- possibility generating a random cookie

- update existing conf files with fixes for CVE-2022-24706
diff --git a/bin/build_installer.ps1 b/bin/build_installer.ps1
index 2bde102..d528125 100644
--- a/bin/build_installer.ps1
+++ b/bin/build_installer.ps1
@@ -69,9 +69,10 @@
 candle -arch x64 "-dCouchDir=${CouchDB}" couchdbfiles.wxs
 candle -arch x64 -ext WiXUtilExtension couchdb_wixui.wxs
 candle -arch x64 -ext WiXUtilExtension adminprompt.wxs
+candle -arch x64 -ext WiXUtilExtension cookieprompt.wxs
 candle -arch x64 -ext WiXUtilExtension customexit.wxs
 candle -arch x64 -ext WiXUtilExtension CouchInstallDirDlg.wxs
-light -sw1076 -sice:ICE17 -ext WixUIExtension -ext WiXUtilExtension "-cultures:en-us;en;neutral" adminprompt.wixobj couchdb.wixobj couchdbfiles.wixobj couchdb_wixui.wixobj customexit.wixobj CouchInstallDirDlg.wixobj -out apache-couchdb-${CouchDBVersion}.msi
+light -sw1076 -sice:ICE17 -ext WixUIExtension -ext WiXUtilExtension "-cultures:en-us;en;neutral" adminprompt.wixobj cookieprompt.wixobj couchdb.wixobj couchdbfiles.wixobj couchdb_wixui.wixobj customexit.wixobj CouchInstallDirDlg.wixobj -out apache-couchdb-${CouchDBVersion}.msi
 
 Pop-Location
 
diff --git a/installer/CustomAction/CouchIniAction.cs b/installer/CustomAction/CouchIniAction.cs
index 0b7e0ff..4a82e3f 100644
--- a/installer/CustomAction/CouchIniAction.cs
+++ b/installer/CustomAction/CouchIniAction.cs
@@ -3,101 +3,194 @@
 using System.IO;
 using System.Security.Cryptography;
 using System.Text;
+using System.Text.RegularExpressions;
 using Microsoft.Deployment.WindowsInstaller;
 
 namespace CustomAction
 {
-   public class CustomActions
-   {
-      [CustomAction] public static ActionResult WriteAdminIniFile(Session session)
-      {
-         try
-         {
-            if (!File.Exists(session.CustomActionData["ADMINCONFIGFILE"])) {
-               using (StreamWriter writer = new StreamWriter(session.CustomActionData["ADMINCONFIGFILE"]))
-               {
-                  writer.WriteLine("; CouchDB Windows installer-generated admin user");
-                  writer.WriteLine("[admins]");
-                  writer.WriteLine($"{session.CustomActionData["ADMINUSER"]} = {session.CustomActionData["ADMINPASSWORD"]}");
-               }
-            }
-         }
-         catch (Exception ex)
-         {
-            session.Log("ERROR in custom action WriteAdminIniFile {0}",
-                  ex.ToString());
-            return ActionResult.Failure;
-         }
-
-         return ActionResult.Success;
-      }
-
-      [CustomAction] public static ActionResult MaybeCopyIniFiles(Session session)
-      {
-         try
-         {
-            string[] files = new string[2];
-            files[0] = "vm.args";
-            files[1] = "local.ini";
-
-            foreach (string file in files)
+    public class CustomActions
+    {
+        [CustomAction]
+        public static ActionResult InitCookieValue(Session session)
+        {
+            try
             {
-               if (!File.Exists(Path.Combine(session.CustomActionData["ETCDIR"], file)))
-               {
-                  File.Copy(
-                        Path.Combine(session.CustomActionData["ETCDIR"], file + ".dist"),
-                        Path.Combine(session.CustomActionData["ETCDIR"], file)
-                        );
-               }
+                byte[] buffer = new byte[16];
+                RandomNumberGenerator rng = RNGCryptoServiceProvider.Create();
+                rng.GetBytes(buffer);
+                session["COOKIEVALUE"] = BitConverter.ToString(buffer).Replace("-", String.Empty);
             }
-         }
-         catch (Exception ex)
-         {
-            session.Log("ERROR in custom action MaybeCopyIniFiles {0}",
-                  ex.ToString());
-            return ActionResult.Failure;
-         }
-         return ActionResult.Success;
-      }
-
-      [CustomAction] public static ActionResult MaybeRemoveUserConfig(Session session)
-      {
-         try
-         {
-            string[] files = new string[2];
-            files[0] = "vm.args";
-            files[1] = "local.ini";
-
-            foreach (string file in files)
+            catch (Exception ex)
             {
-               if (File.Exists(Path.Combine(session.CustomActionData["ETCDIR"], file)) &&
-                     File.Exists(Path.Combine(session.CustomActionData["ETCDIR"], file + ".dist")))
-               {
-                  if (GetChecksum(Path.Combine(session.CustomActionData["ETCDIR"], file)) ==
-                        GetChecksum(Path.Combine(session.CustomActionData["ETCDIR"], file + ".dist")))
-                  {
-                     File.Delete(Path.Combine(session.CustomActionData["ETCDIR"], file));
-                  }
-               }
+                session.Log("ERROR in custom action InitCookieValue {0}",
+                      ex.ToString());
+                return ActionResult.Failure;
             }
-         }
-         catch (Exception ex)
-         {
-            session.Log("ERROR in custom action MaybeRemoveUserConfig {0}",
-                  ex.ToString());
-            return ActionResult.Failure;
-         }
-         return ActionResult.Success;
-      }
 
-      private static string GetChecksum(string file)
-      {
-         using (FileStream stream = File.OpenRead(file))
-         {
-            SHA256Managed sha = new SHA256Managed();
-            byte[] checksum = sha.ComputeHash(stream);
-            return BitConverter.ToString(checksum).Replace("-", String.Empty);
-         }
-      }
-   }
+            return ActionResult.Success;
+        }
+        [CustomAction]
+        public static ActionResult WriteAdminIniFile(Session session)
+        {
+            try
+            {
+                if (!File.Exists(session.CustomActionData["ADMINCONFIGFILE"]))
+                {
+                    using (StreamWriter writer = new StreamWriter(session.CustomActionData["ADMINCONFIGFILE"]))
+                    {
+                        writer.WriteLine("; CouchDB Windows installer-generated admin user");
+                        writer.WriteLine("[admins]");
+                        writer.WriteLine($"{session.CustomActionData["ADMINUSER"]} = {session.CustomActionData["ADMINPASSWORD"]}");
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                session.Log("ERROR in custom action WriteAdminIniFile {0}",
+                      ex.ToString());
+                return ActionResult.Failure;
+            }
+
+            return ActionResult.Success;
+        }
+
+        [CustomAction]
+        public static ActionResult WriteCookieToVmArgs(Session session)
+        {
+            try
+            {
+                string VMARGSFILE = Path.Combine(session.CustomActionData["ETCDIR"], "vm.args");
+                if (File.Exists(VMARGSFILE))
+                {
+                    session.Log("Patching erlang cookie in existing vm.args file");
+                    PatchErlangCookie(session, VMARGSFILE);
+                }
+
+                string VMFile = Path.Combine(session.CustomActionData["ETCDIR"], "vm.args.dist");
+                byte[] VMBuffer = File.ReadAllBytes(VMFile);
+                string VMText = Regex.Replace(Encoding.UTF8.GetString(VMBuffer), @"# -setcookie", $"-setcookie {session.CustomActionData["COOKIEVALUE"]}");
+                File.WriteAllBytes(VMFile, Encoding.UTF8.GetBytes(VMText));
+            }
+            catch (Exception ex)
+            {
+                session.Log("ERROR in custom action WriteCookieToVmArgs {0}",
+                      ex.ToString());
+                return ActionResult.Failure;
+            }
+
+            return ActionResult.Success;
+        }
+
+        private static void PatchErlangCookie(Session session, string file)
+        {
+            //Patching erlang cookie
+            byte[] VMBuffer = File.ReadAllBytes(file);
+            string VMText = Regex.Replace(Encoding.UTF8.GetString(VMBuffer), @"-setcookie \S*", $"-setcookie {session.CustomActionData["COOKIEVALUE"]}");
+            File.WriteAllBytes(file, Encoding.UTF8.GetBytes(VMText));
+        }
+
+        private static void PatchErlangInterface(Session session, string file)
+        {
+            byte[] VMBuffer = File.ReadAllBytes(file);
+
+            //Patching erlang interface
+            string pattern = @"-kernel inet_dist_use_interface";
+            string input = Encoding.UTF8.GetString(VMBuffer);
+            Match m = Regex.Match(input, pattern, RegexOptions.IgnoreCase);
+            if (!m.Success)
+            {
+                session.Log("Pattern \"inet_dist_use_interface\" not found, appending fix.");
+
+                using (StreamWriter sw = File.AppendText(file))
+                {
+                    sw.WriteLine();
+                    sw.WriteLine("# Which interfaces should the node listen on?");
+                    sw.WriteLine("-kernel inet_dist_use_interface {127,0,0,1}");
+                    sw.Close();
+                }
+            }
+            else
+            {
+                session.Log("Pattern \"inet_dist_use_interface\" found, skipping.");
+            }
+        }
+
+        [CustomAction]
+        public static ActionResult MaybeCopyIniFiles(Session session)
+        {
+            try
+            {
+                string[] files = new string[2];
+                files[0] = "vm.args";
+                files[1] = "local.ini";
+
+                string VMARGSFILE = Path.Combine(session.CustomActionData["ETCDIR"], files[0]);
+
+                if (File.Exists(VMARGSFILE))
+                {
+                    session.Log("Patching erlang interface in existing vm.args file");
+                    PatchErlangInterface(session, VMARGSFILE);
+                }
+
+                foreach (string file in files)
+                {
+                    if (!File.Exists(Path.Combine(session.CustomActionData["ETCDIR"], file)))
+                    {
+                        File.Copy(
+                              Path.Combine(session.CustomActionData["ETCDIR"], file + ".dist"),
+                              Path.Combine(session.CustomActionData["ETCDIR"], file)
+                              );
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                session.Log("ERROR in custom action MaybeCopyIniFiles {0}",
+                      ex.ToString());
+                return ActionResult.Failure;
+            }
+            return ActionResult.Success;
+        }
+
+        [CustomAction]
+        public static ActionResult MaybeRemoveUserConfig(Session session)
+        {
+            try
+            {
+                string[] files = new string[2];
+                files[0] = "vm.args";
+                files[1] = "local.ini";
+
+                foreach (string file in files)
+                {
+                    if (File.Exists(Path.Combine(session.CustomActionData["ETCDIR"], file)) &&
+                          File.Exists(Path.Combine(session.CustomActionData["ETCDIR"], file + ".dist")))
+                    {
+                        if (GetChecksum(Path.Combine(session.CustomActionData["ETCDIR"], file)) ==
+                              GetChecksum(Path.Combine(session.CustomActionData["ETCDIR"], file + ".dist")))
+                        {
+                            File.Delete(Path.Combine(session.CustomActionData["ETCDIR"], file));
+                        }
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                session.Log("ERROR in custom action MaybeRemoveUserConfig {0}",
+                      ex.ToString());
+                return ActionResult.Failure;
+            }
+            return ActionResult.Success;
+        }
+
+        private static string GetChecksum(string file)
+        {
+            using (FileStream stream = File.OpenRead(file))
+            {
+                SHA256Managed sha = new SHA256Managed();
+                byte[] checksum = sha.ComputeHash(stream);
+                return BitConverter.ToString(checksum).Replace("-", String.Empty);
+            }
+        }
+    }
 }
diff --git a/installer/CustomAction/CustomAction.config b/installer/CustomAction/CustomAction.config
index 16a542b..6bf9448 100644
--- a/installer/CustomAction/CustomAction.config
+++ b/installer/CustomAction/CustomAction.config
@@ -7,6 +7,6 @@
  -->
   <configuration>
      <startup useLegacyV2RuntimeActivationPolicy="true">
-        <supportedRuntime version="v2.0.50727" />
+        <supportedRuntime version="v4.0" />
      </startup>
   </configuration>
diff --git a/installer/CustomAction/CustomAction.csproj b/installer/CustomAction/CustomAction.csproj
index 7b6bd51..80399ef 100644
--- a/installer/CustomAction/CustomAction.csproj
+++ b/installer/CustomAction/CustomAction.csproj
@@ -10,7 +10,7 @@
     <AppDesignerFolder>Properties</AppDesignerFolder>
     <RootNamespace>CustomAction</RootNamespace>
     <AssemblyName>CouchIniAction</AssemblyName>
-    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+    <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
     <FileAlignment>512</FileAlignment>
     <WixCATargetsPath Condition=" '$(WixCATargetsPath)' == '' ">C:\Program Files (x86)\MSBuild\Microsoft\WiX\v3.x\wix.ca.targets</WixCATargetsPath>
   </PropertyGroup>
diff --git a/installer/cookieprompt.wxs b/installer/cookieprompt.wxs
new file mode 100644
index 0000000..844ef85
--- /dev/null
+++ b/installer/cookieprompt.wxs
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
+   <Fragment>
+      <Property Id="COOKIEVALUE" Hidden="yes" />
+      <UI>
+         <Dialog Id="CookiePromptDlg" Width="370" Height="270" Title="Set Cookie value">
+            <Control Id="Title" Type="Text" X="20" Y="20" Width="220" Height="30" Transparent="yes" NoPrefix="yes" Text="{\WixUI_Font_Emphasized}Set Cookie value" />
+            <Control Id="Description" Type="Text" X="20" Y="50" Width="220" Height="40" Transparent="yes" NoPrefix="yes" Text="{\WixUI_Font_Emphasized}For security reasons, the cookie value that CouchDB instances use to communicate between each other needs to be set even in standalone mode." />
+            <Control Id="Label" Type="Text" Width="322" Height="10" X="20" Y="110" Text="Cookie value:" />
+            <Control Id="Textbox" Type="Edit" Width="200" Height="15" X="20" Y="123" Property="COOKIEVALUE" />
+	    <Control Id="Validate" Type="PushButton" Width="100" Height="15" X="140" Y="180" Text="Validate Cookie" />
+	    <Control Id="Random" Type="PushButton" Width="100" Height="15" X="20" Y="180" Text="Random Cookie">
+               <Publish Event="DoAction" Value="InitCookieValue">1</Publish>
+               <Publish Property="COOKIEVALUE" Value="[COOKIEVALUE]">1</Publish>
+	    </Control>
+            <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
+            <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
+            <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)">
+               <Condition Action="disable"><![CDATA[(COOKIEVALUE = "") OR (COOKIEVALUE >< " ")]]></Condition>
+               <Condition Action="enable"><![CDATA[(COOKIEVALUE <> "") AND NOT (COOKIEVALUE >< " ")]]></Condition>
+
+               <Publish Property="COOKIEVALUE" Value="[COOKIEVALUE]">1</Publish>
+            </Control>
+            <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
+               <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
+            </Control>
+         </Dialog>
+      </UI>
+   </Fragment>
+</Wix>
+
diff --git a/installer/couchdb.wxs.in b/installer/couchdb.wxs.in
index 390ac23..f979547 100644
--- a/installer/couchdb.wxs.in
+++ b/installer/couchdb.wxs.in
@@ -48,7 +48,7 @@
       </Directory>
 
       <DirectoryRef Id="TARGETDIR">
-         <Merge Id="VCRedist141" SourceFile="C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Redist\MSVC\14.16.27012\MergeModules\Microsoft_VC141_CRT_x64.msm" DiskId="1" Language="0"/>
+         <Merge Id="VCRedist143" SourceFile="C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Redist\MSVC\14.31.31103\MergeModules\Microsoft_VC143_CRT_x64.msm" DiskId="1" Language="0"/>
       </DirectoryRef>
 
       <SetProperty Id="ARPINSTALLLOCATION" Value="[APPLICATIONFOLDER]"
@@ -105,8 +105,8 @@
          <ComponentGroupRef Id="CouchDBFilesGroup" />
       </Feature>
 
-      <Feature Id="VCRedist141" Title="Visual C++ 14.1 Runtime" AllowAdvertise="no" Display="hidden" Level="1">
-         <MergeRef Id="VCRedist141"/>
+      <Feature Id="VCRedist143" Title="Microsoft Visual C++ 2015-2022 Redistributable (x64)" AllowAdvertise="no" Display="hidden" Level="1">
+         <MergeRef Id="VCRedist143"/>
       </Feature>
 
       <!--Custom actions and execute sequences-->
@@ -119,6 +119,19 @@
          Value="ADMINCONFIGFILE=[APPLICATIONFOLDER]etc\local.d\10-admins.ini;ADMINUSER=[ADMINUSER];ADMINPASSWORD=[ADMINPASSWORD]"
          />
       <CustomAction
+         Id="SetCookieValueInVmArgsPropertyValues"
+         Property="WriteCookieToVmArgs"
+         Value="ETCDIR=[APPLICATIONFOLDER]etc;COOKIEVALUE=[COOKIEVALUE]"
+         />
+      <CustomAction
+         Id="InitCookieValue"
+         Return="check"
+         DllEntry="InitCookieValue"
+         HideTarget="yes"
+         BinaryKey="CouchIniActionDll" 
+         Impersonate="no"
+         />
+      <CustomAction
          Id="WriteAdminIniFile"
          Return="check"
          Execute="deferred"
@@ -127,6 +140,15 @@
          BinaryKey="CouchIniActionDll" 
          Impersonate="no"
          />
+      <CustomAction
+         Id="WriteCookieToVmArgs"
+         Return="check"
+         Execute="deferred"
+         DllEntry="WriteCookieToVmArgs"
+         HideTarget="yes"
+         BinaryKey="CouchIniActionDll" 
+         Impersonate="no"
+         />
 
       <CustomAction
          Id="SetMaybeCopyIniFilesValues"
@@ -252,8 +274,10 @@
       <Property Id="SCHEDULEREBOOT" Value="0" />
       <InstallExecuteSequence>
          <Custom Action="SetAdminIniCustomActionPropertyValues" After="InstallFiles">NOT Installed AND NOT REMOVE</Custom>
-         <Custom Action="WriteAdminIniFile" After="SetAdminIniCustomActionPropertyValues">NOT Installed AND NOT REMOVE</Custom>
-         <Custom Action="SetMaybeCopyIniFilesValues" After="WriteAdminIniFile">NOT Installed AND NOT REMOVE</Custom>
+         <Custom Action="SetCookieValueInVmArgsPropertyValues" After="SetAdminIniCustomActionPropertyValues">NOT Installed AND NOT REMOVE</Custom>
+         <Custom Action="WriteAdminIniFile" After="SetCookieValueInVmArgsPropertyValues">NOT Installed AND NOT REMOVE</Custom>
+         <Custom Action="WriteCookieToVmArgs" After="WriteAdminIniFile">NOT Installed AND NOT REMOVE</Custom>
+         <Custom Action="SetMaybeCopyIniFilesValues" After="WriteCookieToVmArgs">NOT Installed AND NOT REMOVE</Custom>
          <Custom Action="MaybeCopyIniFiles" After="SetMaybeCopyIniFilesValues">NOT Installed AND NOT REMOVE</Custom>
          <Custom Action="InstallCouchDBService" After="MaybeCopyIniFiles">INSTALLSERVICE AND NOT Installed AND NOT REMOVE</Custom>
          <Custom Action="StartCouchDBService" After="InstallCouchDBService">INSTALLSERVICE AND NOT Installed AND NOT REMOVE</Custom>
diff --git a/installer/couchdb_wixui.wxs b/installer/couchdb_wixui.wxs
index 453777f..336d5f3 100644
--- a/installer/couchdb_wixui.wxs
+++ b/installer/couchdb_wixui.wxs
@@ -44,9 +44,11 @@
             <Publish Dialog="CouchInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
 
             <Publish Dialog="AdminPromptDlg" Control="Back" Event="NewDialog" Value="CouchInstallDirDlg">1</Publish>
-            <Publish Dialog="AdminPromptDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
+            <Publish Dialog="AdminPromptDlg" Control="Next" Event="NewDialog" Value="CookiePromptDlg">1</Publish>
+            <Publish Dialog="CookiePromptDlg" Control="Back" Event="NewDialog" Value="AdminPromptDlg">1</Publish>
+            <Publish Dialog="CookiePromptDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
             
-            <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="AdminPromptDlg" Order="1">NOT Installed</Publish>
+            <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="CookiePromptDlg" Order="1">NOT Installed</Publish>
             <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
             <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>