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