From 5a3774c72e5b2dee8d31879c82eec733ac6c154f Mon Sep 17 00:00:00 2001
From: Nasar Khan <nasarak98@gmail.com>
Date: Wed, 17 Jul 2024 11:39:58 -0400
Subject: [PATCH] add messaging hostname validation

---
 .../message_configuration.rb                  | 24 ++++++++-
 .../message_configuration_client.rb           |  2 +-
 .../message_configuration_server.rb           | 15 ++----
 lib/manageiq/appliance_console/prompts.rb     |  6 +++
 spec/message_configuration_client_spec.rb     |  5 +-
 spec/message_configuration_server_spec.rb     | 54 ++-----------------
 6 files changed, 41 insertions(+), 65 deletions(-)

diff --git a/lib/manageiq/appliance_console/message_configuration.rb b/lib/manageiq/appliance_console/message_configuration.rb
index 53110248..b77f5a4d 100644
--- a/lib/manageiq/appliance_console/message_configuration.rb
+++ b/lib/manageiq/appliance_console/message_configuration.rb
@@ -68,7 +68,7 @@ def ask_questions
         show_parameters
         return false unless agree("\nProceed? (Y/N): ")
 
-        return false unless host_reachable?(message_server_host, "Message Server Host:")
+        return false unless host_resolvable?(message_server_host) && host_reachable?(message_server_host, "Message Server Host:")
 
         true
       end
@@ -190,6 +190,28 @@ def host_reachable?(host, what)
         true
       end
 
+      def host_resolvable?(host)
+        require 'resolv'
+
+        say("Checking if #{host} is resolvable ... ")
+        begin
+          ip_address = Resolv.getaddress(host)
+
+          if IPAddr.new("127.0.0.1/8").include?(ip_address) || IPAddr.new("::1/128").include?(ip_address)
+            say("Failed.\nThe hostname must not resolve to a link-local address")
+
+            return false
+          end
+        rescue Resolv::ResolvError => e
+          say("Failed.\nHostname #{host} is not resolvable: #{e.message}")
+
+          return false
+        end
+
+        say("Succeeded.")
+        true
+      end
+
       def unconfigure
         remove_installed_files
       end
diff --git a/lib/manageiq/appliance_console/message_configuration_client.rb b/lib/manageiq/appliance_console/message_configuration_client.rb
index caf10689..3a43d44f 100644
--- a/lib/manageiq/appliance_console/message_configuration_client.rb
+++ b/lib/manageiq/appliance_console/message_configuration_client.rb
@@ -46,7 +46,7 @@ def configure
       def ask_for_parameters
         say("\nMessage Client Parameters:\n\n")
 
-        @message_server_host         = ask_for_string("Message Server Hostname or IP address")
+        @message_server_host         = ask_for_messaging_hostname("Message Server Hostname")
         @message_server_port         = ask_for_integer("Message Server Port number", (1..65_535), 9_093).to_i
         @message_server_username     = ask_for_string("Message Server Username", message_server_username)
         @message_server_password     = ask_for_password("Message Server Password")
diff --git a/lib/manageiq/appliance_console/message_configuration_server.rb b/lib/manageiq/appliance_console/message_configuration_server.rb
index 44a4c2ec..6532cbdd 100644
--- a/lib/manageiq/appliance_console/message_configuration_server.rb
+++ b/lib/manageiq/appliance_console/message_configuration_server.rb
@@ -68,11 +68,7 @@ def restart_services
       def ask_for_parameters
         say("\nMessage Server Parameters:\n\n")
 
-        @message_server_host       = ask_for_string("Message Server Hostname or IP address", message_server_host)
-
-        # SSL Validation for Kafka does not work for hostnames containing "localhost"
-        # Therefore we replace with the equivalent IP "127.0.0.1" if a /localhost*/ hostname was entered
-        @message_server_host       = "127.0.0.1" if @message_server_host.include?("localhost")
+        @message_server_host       = ask_for_messaging_hostname("Message Server Hostname", message_server_host)
 
         @message_keystore_username = ask_for_string("Message Keystore Username", message_keystore_username)
         @message_keystore_password = ask_for_new_password("Message Keystore Password")
@@ -301,13 +297,8 @@ def assemble_keystore_params
                            "-genkey"   => nil,
                            "-keyalg"   => "RSA"}
 
-        if message_server_host.ipaddress?
-          keystore_params["-alias"] = "localhost"
-          keystore_params["-ext"] = "san=ip:#{message_server_host}"
-        else
-          keystore_params["-alias"] = message_server_host
-          keystore_params["-ext"] = "san=dns:#{message_server_host}"
-        end
+        keystore_params["-alias"] = message_server_host
+        keystore_params["-ext"] = "san=dns:#{message_server_host}"
 
         keystore_params["-dname"] = "cn=#{keystore_params["-alias"]}"
 
diff --git a/lib/manageiq/appliance_console/prompts.rb b/lib/manageiq/appliance_console/prompts.rb
index c01429f9..bf279962 100644
--- a/lib/manageiq/appliance_console/prompts.rb
+++ b/lib/manageiq/appliance_console/prompts.rb
@@ -14,6 +14,7 @@ module Prompts
     INT_REGEXP    = /^[0-9]+$/
     NONE_REGEXP   = /^('?NONE'?)?$/i.freeze
     HOSTNAME_REGEXP = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/.freeze
+    MESSAGING_HOSTNAME_REGEXP = /^(?!.*localhost)(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/.freeze
 
     def ask_for_uri(prompt, expected_scheme, opts = {})
       require 'uri'
@@ -71,6 +72,11 @@ def ask_for_hostname(prompt, default = nil, validate = HOSTNAME_REGEXP, error_te
       just_ask(prompt, default, validate, error_text, &block)
     end
 
+    def ask_for_messaging_hostname(prompt, default = nil, error_text = "a valid Messaging Hostname (not an IP or localhost)", &block)
+      validation = ->(h) { h =~ MESSAGING_HOSTNAME_REGEXP && h !~ IP_REGEXP }
+      just_ask(prompt, default, validation, error_text, &block)
+    end
+
     def ask_for_ip_or_hostname(prompt, default = nil)
       validation = ->(h) { (h =~ HOSTNAME_REGEXP || h =~ IP_REGEXP) && h.length > 0 }
       ask_for_ip(prompt, default, validation, "a valid Hostname or IP Address.")
diff --git a/spec/message_configuration_client_spec.rb b/spec/message_configuration_client_spec.rb
index b202a9dd..8876849d 100644
--- a/spec/message_configuration_client_spec.rb
+++ b/spec/message_configuration_client_spec.rb
@@ -40,12 +40,13 @@
   describe "#ask_questions" do
     before do
       allow(subject).to receive(:agree).and_return(true)
+      allow(subject).to receive(:host_resolvable?).and_return(true)
       allow(subject).to receive(:host_reachable?).and_return(true)
       allow(subject).to receive(:message_client_configured?).and_return(false)
     end
 
     it "should prompt for message_keystore_username and message_keystore_password" do
-      expect(subject).to receive(:ask_for_string).with("Message Server Hostname or IP address").and_return("my-host-name.example.com")
+      expect(subject).to receive(:ask_for_messaging_hostname).with("Message Server Hostname").and_return("my-host-name.example.com")
       expect(subject).to receive(:ask_for_integer).with("Message Server Port number", (1..65_535), 9_093).and_return("9093")
       expect(subject).to receive(:ask_for_string).with("Message Keystore Username", message_keystore_username).and_return("admin")
       expect(subject).to receive(:ask_for_password).with("Message Keystore Password").and_return("top_secret")
@@ -61,7 +62,7 @@
     end
 
     it "should display Server Hostname and Key Username" do
-      allow(subject).to receive(:ask_for_string).with("Message Server Hostname or IP address").and_return("my-kafka-server.example.com")
+      allow(subject).to receive(:ask_for_messaging_hostname).with("Message Server Hostname").and_return("my-kafka-server.example.com")
       allow(subject).to receive(:ask_for_integer).with("Message Server Port number", (1..65_535), 9_093).and_return("9093")
       allow(subject).to receive(:ask_for_string).with("Message Keystore Username", message_keystore_username).and_return("admin")
       allow(subject).to receive(:ask_for_password).with("Message Keystore Password").and_return("top_secret")
diff --git a/spec/message_configuration_server_spec.rb b/spec/message_configuration_server_spec.rb
index 6952a7a5..d6a64cfa 100644
--- a/spec/message_configuration_server_spec.rb
+++ b/spec/message_configuration_server_spec.rb
@@ -35,6 +35,7 @@
   describe "#ask_questions" do
     before do
       allow(subject).to receive(:agree).and_return(true)
+      allow(subject).to receive(:host_resolvable?).and_return(true)
       allow(subject).to receive(:host_reachable?).and_return(true)
       allow(subject).to receive(:message_server_configured?).and_return(false)
     end
@@ -45,7 +46,7 @@
       end
 
       it "should prompt for message_keystore_username and message_keystore_password" do
-        expect(subject).to receive(:ask_for_string).with("Message Server Hostname or IP address", "my-host-name.example.com").and_return("my-host-name.example.com")
+        expect(subject).to receive(:ask_for_messaging_hostname).with("Message Server Hostname", "my-host-name.example.com").and_return("my-host-name.example.com")
         expect(subject).to receive(:ask_for_string).with("Message Keystore Username", message_keystore_username).and_return("admin")
         expect(subject).to receive(:just_ask).with(/Message Keystore Password/i, anything).twice.and_return("top_secret")
 
@@ -55,7 +56,7 @@
       end
 
       it "should re-prompt when an empty message_keystore_password is given" do
-        expect(subject).to receive(:ask_for_string).with("Message Server Hostname or IP address", "my-host-name.example.com").and_return("my-host-name.example.com")
+        expect(subject).to receive(:ask_for_messaging_hostname).with("Message Server Hostname", "my-host-name.example.com").and_return("my-host-name.example.com")
         expect(subject).to receive(:ask_for_string).with("Message Keystore Username", message_keystore_username).and_return("admin")
         expect(subject).to receive(:just_ask).with(/Message Keystore Password/i, anything).and_return("")
         expect(subject).to receive(:just_ask).with(/Message Keystore Password/i, anything).twice.and_return("top_secret")
@@ -67,7 +68,7 @@
       end
 
       it "should display Server Hostname and Keystore Username" do
-        allow(subject).to receive(:ask_for_string).with("Message Server Hostname or IP address", "my-host-name.example.com").and_return("my-host-name.example.com")
+        allow(subject).to receive(:ask_for_messaging_hostname).with("Message Server Hostname", "my-host-name.example.com").and_return("my-host-name.example.com")
         allow(subject).to receive(:ask_for_string).with("Message Keystore Username", message_keystore_username).and_return("admin")
         expect(subject).to receive(:just_ask).with(/Message Keystore Password/i, anything).twice.and_return("top_secret")
 
@@ -88,7 +89,7 @@
 
       it "should prompt for message_keystore_username, message_keystore_password and persistent disk" do
         message_persistent_disk = LinuxAdmin::Disk.new(:path => "/tmp/disk")
-        expect(subject).to receive(:ask_for_string).with("Message Server Hostname or IP address", "my-host-name.example.com").and_return("my-host-name.example.com")
+        expect(subject).to receive(:ask_for_messaging_hostname).with("Message Server Hostname", "my-host-name.example.com").and_return("my-host-name.example.com")
         expect(subject).to receive(:ask_for_string).with("Message Keystore Username", message_keystore_username).and_return("admin")
         expect(subject).to receive(:just_ask).with(/Message Keystore Password/i, anything).twice.and_return("top_secret")
         expect(subject).to receive(:ask_for_disk).with("Persistent disk").and_return(message_persistent_disk)
@@ -215,14 +216,6 @@
       expect(subject).to receive(:say).with("Configure Keystore")
     end
 
-    context "with IP address" do
-      let(:ks_alias) { "localhost" }
-      let(:message_server_host) { "192.0.2.0" }
-      let(:ext) { "san=ip:#{message_server_host}" }
-
-      include_examples "configure keystore"
-    end
-
     context "with hostname" do
       let(:ks_alias) { "my-host-name.example.com" }
       let(:message_server_host) { ks_alias }
@@ -283,13 +276,6 @@
       end
     end
 
-    context "with IP address" do
-      let(:ident_algorithm) { "" }
-      let(:client_auth) { "none" }
-      let(:message_server_host) { "192.0.2.0" }
-      include_examples "service properties file"
-    end
-
     context "with hostname" do
       let(:ident_algorithm) { "HTTPS" }
       let(:client_auth) { "required" }
@@ -366,35 +352,5 @@
         expect(subject.message_server_host).to eq("192.0.2.1")
       end
     end
-
-    context "when --message-server-host is specified as localhost*" do
-      before do
-        allow(subject).to receive(:agree).and_return(true)
-        allow(subject).to receive(:host_reachable?).and_return(true)
-        allow(subject).to receive(:message_server_configured?).and_return(true)
-      end
-
-      it "replaces localhost with 127.0.0.1" do
-        expect(subject).to receive(:say).with(/Message Server Parameters/)
-        expect(subject).to receive(:ask_for_string).with("Message Server Hostname or IP address", anything).and_return("localhost")
-        expect(subject).to receive(:ask_for_string).with("Message Keystore Username", anything).and_return("admin")
-        expect(subject).to receive(:just_ask).with(/Message Keystore Password/i, anything).twice.and_return("top_secret")
-        expect(subject).to receive(:ask_for_disk).with("Persistent disk").and_return("/tmp/disk")
-
-        subject.ask_for_parameters
-        expect(subject.message_server_host).to eq("127.0.0.1")
-      end
-
-      it "replaces localhost.localadmin with 127.0.0.1" do
-        expect(subject).to receive(:say).with(/Message Server Parameters/)
-        expect(subject).to receive(:ask_for_string).with("Message Server Hostname or IP address", anything).and_return("localhost.localadmin")
-        expect(subject).to receive(:ask_for_string).with("Message Keystore Username", anything).and_return("admin")
-        expect(subject).to receive(:just_ask).with(/Message Keystore Password/i, anything).twice.and_return("top_secret")
-        expect(subject).to receive(:ask_for_disk).with("Persistent disk").and_return("/tmp/disk")
-
-        subject.ask_for_parameters
-        expect(subject.message_server_host).to eq("127.0.0.1")
-      end
-    end
   end
 end