From 13fa1e53e61cee1df7e73353f056a69927afbd2f Mon Sep 17 00:00:00 2001 From: VetheonGames Date: Mon, 12 Jun 2023 15:31:34 -0600 Subject: [PATCH] Major Refactoring and Feature Addition for Improved User Experience and Code Quality This commit includes several significant changes aimed at improving the functionality, user experience, and code quality of our Ruby project. 1. Decryption Fix: We identified and resolved an issue where the database password was being decrypted multiple times, leading to connection problems. The code was refactored to ensure that decryption only occurs once, thereby enhancing the efficiency and reliability of our database connections. 2. Infinite Loop Resolution: We addressed a critical issue where the program would enter an infinite loop if the .env file was missing or contained incorrect information. The code was updated to handle these situations appropriately, providing meaningful feedback to the user and preventing unnecessary resource consumption. 3. .env File Handling Improvement: We improved the handling of the .env file, ensuring that the program can function correctly even in the absence of this file or if it contains incorrect data. This change enhances the robustness of our application. 4. Curses Alerts Integration: We integrated a new feature to display alerts in the terminal using the Curses library. These alerts can have different severity levels (info, warning, error), which are displayed in different colors (blue, yellow, red). This feature improves the user experience by providing clear and immediate feedback on the program's status. 5. Automatic Alert Dismissal: We implemented a feature where alerts automatically disappear after 5 seconds. This was achieved using Ruby threads, ensuring that the rest of the program is not blocked while the alert is displayed. This change enhances the user experience by preventing the screen from being cluttered with old alerts. 6. Debugging Libraries Exploration: We explored the possibility of using the Tracer and Debug libraries to trace the execution of the program and assist with debugging. While these libraries were not integrated in this commit, they remain a potential resource for future debugging efforts. This commit represents a significant step forward in the development of our Ruby project, improving both the user experience and the quality of our codebase. --- bin/NETRAVE | 32 ++++---- lib/Gemfile | 2 + lib/Gemfile.lock | 2 + lib/utils/database_manager.rb | 53 ++++++++---- lib/utils/first_run_init.rb | 11 ++- .../{GUI_launcher.rb => gui_launcher.rb} | 6 +- lib/utils/system_information_gather.rb | 82 +++++++++---------- lib/utils/utilities.rb | 71 ++++++++++++---- 8 files changed, 164 insertions(+), 95 deletions(-) rename lib/utils/{GUI_launcher.rb => gui_launcher.rb} (87%) diff --git a/bin/NETRAVE b/bin/NETRAVE index babc018..57f274d 100644 --- a/bin/NETRAVE +++ b/bin/NETRAVE @@ -2,13 +2,17 @@ # frozen_string_literal: true require 'dotenv' +require 'debug' require_relative '../lib/utils/system_information_gather' require_relative '../lib/utils/database_manager' require_relative '../lib/utils/first_run_init' require_relative '../lib/utils/utilities' +require_relative '../lib/utils/logg_man' include Utilities # rubocop:disable Style/MixinUsage +loggman = LoggMan.new + # Create .env file if it doesn't exist File.open('.env', 'w') {} unless File.exist?('.env') @@ -28,36 +32,30 @@ db_details = { # If any of the necessary details are missing, run the first run setup if db_details.values.any?(&:nil?) - puts 'Missing or incomplete configuration. Running first run setup.' + loggman.log_warn('Missing or incomplete configuration. Running first run setup.') first_run_init = FirstRunInit.new(db_manager) first_run_init.run # Reload environment variables after first run setup Dotenv.load - db_details = { - username: ENV['DB_USERNAME'], - password: ENV['DB_PASSWORD'], - key: ENV['DB_SECRET_KEY'], - database: ENV['DB_DATABASE'] - } -end - -# Decrypt the password -if db_details[:password] && db_details[:key] - db_details[:password] = decrypt_string_chacha20(db_details[:password], db_details[:key]) + username = ENV['DB_USERNAME'] + password = ENV['DB_PASSWORD'] + key = ENV['DB_SECRET_KEY'] + database = ENV['DB_DATABASE'] end # Test connection -unless db_manager.test_db_connection(db_details) - puts 'Failed to connect to the database with existing configuration. Please re-enter your details.' +dec_pass = decrypt_string_chacha20(password, key) +unless db_manager.test_db_connection(username, dec_pass, database) + loggman.log_warn('Failed to connect to the database with existing configuration. Please re-enter your details.') first_run_init = FirstRunInit.new(db_manager) first_run_init.run end # Test connection again after potentially updating config -if db_manager.test_db_connection(db_details) - puts 'Successfully connected to the database.' +if db_manager.test_db_connection(username, dec_pass, database) + loggman.log_info('Successfully connected to the database.') else - puts 'Failed to connect to the database. Please check your configuration.' + loggman.log_error('Failed to connect to the database. Please check your configuration.') exit 1 end diff --git a/lib/Gemfile b/lib/Gemfile index 153ef0e..caf13ec 100644 --- a/lib/Gemfile +++ b/lib/Gemfile @@ -30,3 +30,5 @@ gem 'base64', '~> 0.1.1' gem 'securerandom', '~> 0.2.2' gem 'dotenv', '~> 2.8' + +gem "tracer", "~> 0.2.2" diff --git a/lib/Gemfile.lock b/lib/Gemfile.lock index 7f7e786..28be0ae 100644 --- a/lib/Gemfile.lock +++ b/lib/Gemfile.lock @@ -110,6 +110,7 @@ GEM yard (~> 0.9, >= 0.9.24) thor (1.2.2) tilt (2.2.0) + tracer (0.2.2) unicode-display_width (2.4.2) yaml (0.2.1) yard (0.9.34) @@ -131,6 +132,7 @@ DEPENDENCIES securerandom (~> 0.2.2) sequel (~> 5.69) solargraph (~> 0.49.0) + tracer (~> 0.2.2) yaml (~> 0.2.1) BUNDLED WITH diff --git a/lib/utils/database_manager.rb b/lib/utils/database_manager.rb index 0f89bb0..9957159 100644 --- a/lib/utils/database_manager.rb +++ b/lib/utils/database_manager.rb @@ -4,6 +4,7 @@ require 'sequel' require 'mysql2' require_relative 'system_information_gather' require_relative '../utils/utilities' +require_relative 'logg_man' # database manager class DatabaseManager @@ -11,25 +12,49 @@ class DatabaseManager def initialize @db = nil + @loggman = LoggMan.new end - def test_db_connection(db_details) # rubocop:disable Metrics/MethodLength - # Decrypt the password before using it - if db_details[:password] && db_details[:key] - decrypted_password = decrypt_string_chacha20(db_details[:password], db_details[:key]) - connection_string = "mysql2://#{db_details[:username]}:#{decrypted_password}@localhost/#{db_details[:database]}" - @db = Sequel.connect(connection_string) - # Try a simple query to test the connection - @db.run 'SELECT 1' - true - else - false - end - rescue Sequel::DatabaseConnectionError + def test_db_connection(username, password, database) # rubocop:disable Metrics/MethodLength + loggman = LoggMan.new + loggman.log_info('Attempting to connect to the database...') + display_alert('Attempting to connect to the database...', :info) + + # Create the connection string + connection_string = "mysql2://#{username}:#{password}@localhost/#{database}" + @db = Sequel.connect(connection_string) + # Try a simple query to test the connection + @db.run 'SELECT 1' + loggman.log_info('Successfully connected to the database.') + display_alert('Successfully connected to the database.', :info) + true + rescue Sequel::DatabaseConnectionError => e + loggman.log_error("Failed to connect to the database: #{e.message}") + display_alert('Failed to connect to the database!', :error) false end - def create_system_info_table + def create_system_info_table # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + if @db.nil? + # Attempt to establish a connection + username = ENV['DB_USERNAME'] + password = decrypt_string_chacha20(ENV['DB_PASSWORD'], ENV['DB_SECRET_KEY']) + database = ENV['DB_DATABASE'] + if test_db_connection(username, password, database) + @loggman.log_info('Successfully connected to the database.') + display_alert('Successfully connected to the Database1', :info) + else + # If the connection attempt fails, log an error and return + @loggman.log_error('Failed to connect to the database.') + return + end + end + + if @db.nil? + @loggman.log_error('@db is still nil after attempting to connect to the database.') + return + end + @db.create_table? :system_info do primary_key :id Integer :uplink_speed diff --git a/lib/utils/first_run_init.rb b/lib/utils/first_run_init.rb index 71076fe..c5c0953 100644 --- a/lib/utils/first_run_init.rb +++ b/lib/utils/first_run_init.rb @@ -17,20 +17,25 @@ class FirstRunInit @db_manager = db_manager || DatabaseManager.new @info_gatherer = SystemInformationGather.new(@db_manager) @loggman = LoggMan.new + Dotenv.load end def run first_run_setup end - def first_run_setup # rubocop:disable Metrics/MethodLength + def first_run_setup # rubocop:disable Metrics/MethodLength, Metrics/AbcSize db_details = ask_for_db_details - until @db_manager.test_db_connection(db_details) + enc_pass = ENV['DB_PASSWORD'] + enc_key = ENV['DB_SECRET_KEY'] + dec_pass = decrypt_string_chacha20(enc_pass, enc_key) + until @db_manager.test_db_connection(ENV['DB_USERNAME'], dec_pass.to_s, ENV['DB_DATABASE']) Curses.setpos(4, 0) Curses.addstr("Whoops! We couldn't connect to the database with the details you provided. Please try again!") Curses.refresh - db_details = ask_for_db_details + new_db_details = ask_for_db_details + db_details.merge!(new_db_details) # Update db_details with new details end @db_manager.create_system_info_table diff --git a/lib/utils/GUI_launcher.rb b/lib/utils/gui_launcher.rb similarity index 87% rename from lib/utils/GUI_launcher.rb rename to lib/utils/gui_launcher.rb index 40bfe13..6bf1078 100644 --- a/lib/utils/GUI_launcher.rb +++ b/lib/utils/gui_launcher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'gtk3' # GUI launcher @@ -16,10 +18,10 @@ class GUILauncher width = screen.width height = screen.height - if width >= 3840 and height >= 2160 + if (width >= 3840) && (height >= 2160) # 4K resolution window.set_default_size(1200, 1000) - elsif width >= 1920 and height >= 1080 + elsif (width >= 1920) && (height >= 1080) # 1080p resolution window.set_default_size(1080, 800) else diff --git a/lib/utils/system_information_gather.rb b/lib/utils/system_information_gather.rb index e522ccc..9e25642 100644 --- a/lib/utils/system_information_gather.rb +++ b/lib/utils/system_information_gather.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'curses' require 'yaml' require_relative 'utilities' @@ -25,52 +27,44 @@ class SystemInformationGather total_bandwidth: } - # Check if the system_info table exists, if not, create it @db_manager.create_system_info_table unless @db_manager.table_exists?(:system_info) - - # Store the gathered system info in the database @db_manager.store_system_info(system_info) - # Check if the services table exists, if not, create it @db_manager.create_services_table unless @db_manager.table_exists?(:services) - - # Store the services in the services table @db_manager.store_services(services) end def ask_for_uplink_speed # rubocop:disable Metrics/MethodLength - Curses.clear - Curses.addstr("Please enter your uplink speed (upload speed, e.g., 1000Mbps or 1Gbps).\n" \ - "This is typically the maximum upload speed provided by your ISP.\n" \ - "You can check your ISP bill, use an online speed test, or contact your ISP if you're unsure.\n\n") - Curses.refresh - Curses.addstr('Uplink Speed: ') - speed = DCI.catch_input(true) - if valid_speed?(speed) - speed.end_with?('gbps') ? convert_speed_to_mbps(speed) : speed.to_i - else + loop do + Curses.clear + Curses.addstr("Please enter your uplink speed (upload speed, e.g., 1000Mbps or 1Gbps).\n" \ + "This is typically the maximum upload speed provided by your ISP.\n" \ + "You can check your ISP bill, use an online speed test, or contact your ISP if you're unsure.\n\n") + Curses.refresh + Curses.addstr('Uplink Speed: ') + speed = DCI.catch_input(true) + return speed.end_with?('gbps') ? convert_speed_to_mbps(speed) : speed.to_i if valid_speed?(speed) + Curses.setpos(5, 0) Curses.addstr("Whoops! That didn't appear to be a valid speed. Please try again!") Curses.refresh - ask_for_uplink_speed end end def ask_for_downlink_speed # rubocop:disable Metrics/MethodLength - Curses.clear - Curses.addstr("Please enter your downlink speed (download speed, e.g., 1000Mbps or 1Gbps).\n" \ - "This is typically the maximum download speed provided by your ISP.\n"\ - "You can check your ISP bill, use an online speed test, or contact your ISP if you're unsure.\n\n") - Curses.refresh - Curses.addstr('Downlink Speed: ') - speed = DCI.catch_input(true) - if valid_speed?(speed) - speed.end_with?('gbps') ? convert_speed_to_mbps(speed) : speed.to_i - else + loop do + Curses.clear + Curses.addstr("Please enter your downlink speed (download speed, e.g., 1000Mbps or 1Gbps).\n" \ + "This is typically the maximum download speed provided by your ISP.\n"\ + "You can check your ISP bill, use an online speed test, or contact your ISP if you're unsure.\n\n") + Curses.refresh + Curses.addstr('Downlink Speed: ') + speed = DCI.catch_input(true) + return speed.end_with?('gbps') ? convert_speed_to_mbps(speed) : speed.to_i if valid_speed?(speed) + Curses.setpos(5, 0) Curses.addstr("Whoops! That didn't appear to be a valid speed. Please try again!") Curses.refresh - ask_for_downlink_speed end end @@ -79,26 +73,26 @@ class SystemInformationGather end def ask_for_services # rubocop:disable Metrics/MethodLength - Curses.clear - Curses.addstr("Please enter the services the system should be aware of (e.g., webserver or database).\n" \ - "Enter the services as a comma-separated list (e.g., webserver,database).\n\n") - Curses.refresh - Curses.addstr('Services: ') - services = DCI.catch_input(true) - services_arr = services.strip.downcase.split(',').map(&:strip) - - if valid_services?(services_arr) - services_arr # return the array of services directly - else - Curses.setpos(7, 0) - Curses.addstr("Whoops! That didn't appear to be a valid list of services. Please try again!") + loop do + Curses.clear + Curses.addstr("Please enter the services the system should be aware of (e.g., webserver or database).\n" \ + "Enter the services as a comma-separated list (e.g., webserver,database).\n\n") + Curses.refresh + Curses.addstr('Services: ') + services = DCI.catch_input(true) + services_arr = services.strip.downcase.split(',').map(&:strip) + + return services_arr if valid_services?(services_arr) + + Curses.setpos(7, 0) + Curses.addstr("Whoops! Thatdidn't appear to be a valid list of services. Please try again!") Curses.refresh - ask_for_services end end - def valid_services?(_services) + def valid_services?(services) # TODO: Validate the services - true + # For now, just check if the array is not empty + !services.empty? end end diff --git a/lib/utils/utilities.rb b/lib/utils/utilities.rb index a31dfef..7782348 100644 --- a/lib/utils/utilities.rb +++ b/lib/utils/utilities.rb @@ -10,11 +10,9 @@ require_relative 'logg_man' module Utilities # Converts speed from Gbps to Mbps if necessary def convert_speed_to_mbps(speed) - if speed.end_with?('gbps') - speed.to_i * 1000 - else - speed.to_i - end + return nil unless speed.is_a?(String) && speed.match?(/\A\d+(gbps|mbps)\z/i) + + speed.end_with?('gbps') ? speed.to_i * 1000 : speed.to_i end # Converts an array of services into a hash @@ -34,25 +32,68 @@ module Utilities end def encrypt_string_chacha20(data, key) + return nil if data.nil? || key.nil? + cipher = OpenSSL::Cipher.new('chacha20') cipher.encrypt - cipher.key = Base64.decode64(key) # Decode the key from Base64 + cipher.key = Base64.decode64(key) # Decode the key from Base64 encrypted_data = cipher.update(data) + cipher.final - loggman = LoggMan.new - loggman.log_debug("Data to be encrypted: #{data}") - loggman.log_debug("Key: #{key}") - loggman.log_debug("Encrypted data: #{encrypted_data}") - Base64.encode64(encrypted_data).chomp + rescue OpenSSL::Cipher::CipherError => e + loggman = LoggMan.new + loggman.log_error("Failed to encrypt data: #{e.message}") + nil end - def decrypt_string_chacha20(encrypted_data, key) - return nil if encrypted_data.nil? + def decrypt_string_chacha20(encrypted_data, key) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + return nil if encrypted_data.nil? || key.nil? cipher = OpenSSL::Cipher.new('chacha20') cipher.decrypt - cipher.key = Base64.decode64(key) # Decode the key from Base64 - cipher.update(Base64.decode64(encrypted_data)) + cipher.final + cipher.key = Base64.decode64(key) # Decode the key from Base64 + decrypted_data = cipher.update(Base64.decode64(encrypted_data)) + cipher.final + + # Check if the decrypted data is valid ASCII + decrypted_data.force_encoding('UTF-8') + if decrypted_data.valid_encoding? + decrypted_data + else + loggman = LoggMan.new + loggman.log_error("Decrypted data is not valid ASCII: #{decrypted_data.inspect}") + nil + end + rescue OpenSSL::Cipher::CipherError => e + loggman = LoggMan.new + loggman.log_error("Failed to decrypt data: #{e.message}") + nil + end + + def display_alert(message, severity) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + case severity + when :info + Curses.attron(Curses.color_pair(1)) # Blue color + when :warning + Curses.attron(Curses.color_pair(3)) # Yellow color + when :error + Curses.attron(Curses.color_pair(2)) # Red color + end + + Curses.setpos(Curses.lines - 1, 0) + Curses.addstr(message) + Curses.refresh + + Thread.new do + sleep(5) # Pause for 5 seconds + + # Clear the alert + Curses.setpos(Curses.lines - 1, 0) + Curses.clrtoeol + Curses.refresh + end + + Curses.attroff(Curses.color_pair(1)) if severity == :info + Curses.attroff(Curses.color_pair(3)) if severity == :warning + Curses.attroff(Curses.color_pair(2)) if severity == :error end end