#!/usr/local/bin/ruby40
#
# .$$$     $.                                   .$$$     $.
# $$$$     $$. .$$$  $$$ .$$$$$$.  .$$$$$$$$$$. $$$$     $$. .$$$$$$$. .$$$$$$.
# $ $$     $$$ $ $$  $$$ $ $$$$$$. $$$$$ $$$$$$ $ $$     $$$ $ $$   $$ $ $$$$$$.
# $ `$     $$$ $ `$  $$$ $ `$  $$$ $$' $ `$ `$$ $ `$     $$$ $ `$      $ `$  $$$'
# $. $     $$$ $. $$$$$$ $. $$$$$$ `$  $. $  :' $. $     $$$ $. $$$$   $. $$$$$.
# $::$  .  $$$ $::$  $$$ $::$  $$$     $::$     $::$  .  $$$ $::$      $::$  $$$$
# $;;$ $$$ $$$ $;;$  $$$ $;;$  $$$     $;;$     $;;$ $$$ $$$ $;;$      $;;$  $$$$
# $$$$$$ $$$$$ $$$$  $$$ $$$$  $$$     $$$$     $$$$$$ $$$$$ $$$$$$$$$ $$$$$$$$$'
#
#
# WhatWeb - Next generation web scanner.
# Developed by Andrew Horton (urbanadventurer) and Brendan Coles (bcoles)
#
# Homepage: https://morningstarsecurity.com/research/whatweb
#
# Copyright 2009 to 2025 Andrew Horton and Brendan Coles
#
# This file is part of WhatWeb.
#
# WhatWeb is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# WhatWeb is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with WhatWeb.  If not, see <http://www.gnu.org/licenses/>.

$LOAD_PATH.unshift(File.join(File.expand_path(__dir__)), '.')

# Performance profiling infrastructure (Phase 1)
if ENV['WHATWEB_PROFILE']
  begin
    require 'ruby-prof'
    RubyProf.start
    at_exit do
      result = RubyProf.stop
      printer = RubyProf::FlatPrinter.new(result)
      if ENV['WHATWEB_PROFILE_FILE']
        File.open(ENV['WHATWEB_PROFILE_FILE'], 'w') { |f| printer.print(f) }
        STDERR.puts "Profile saved to #{ENV['WHATWEB_PROFILE_FILE']}"
      else
        printer.print(STDERR)
      end
    end
    STDERR.puts "Ruby profiling enabled"
  rescue LoadError
    STDERR.puts "Warning: ruby-prof gem not available. Install with: gem install ruby-prof"
  end
end

require 'lib/whatweb'

#
# Command-line usage information (full)
#
def usage_full
  puts blue(WhatWeb::BANNER)

  puts(<<-EOT)
WhatWeb - Next generation web scanner version #{WhatWeb::VERSION}.
Developed by Andrew Horton (urbanadventurer) and Brendan Coles (bcoles).
Homepage: https://morningstarsecurity.com/research/whatweb

Usage: whatweb [options] <URLs>

TARGET SELECTION:
  <TARGETs>\t\t\tEnter URLs, hostnames, IP addresses, filenames or
  \t\t\t\tIP ranges in CIDR, x.x.x-x, or x.x.x.x-x.x.x.x
  \t\t\t\tformat.
  --input-file=FILE, -i\t\tRead targets from a file. You can pipe
\t\t\t\thostnames or URLs directly with -i /dev/stdin.

TARGET MODIFICATION:
  --url-prefix\t\t\tAdd a prefix to target URLs.
  --url-suffix\t\t\tAdd a suffix to target URLs.
  --url-pattern\t\t\tInsert the targets into a URL.
\t\t\t\te.g. example.com/%insert%/robots.txt

AGGRESSION:
The aggression level controls the trade-off between speed/stealth and
reliability.
  --aggression, -a=LEVEL\tSet the aggression level. Default: 1.
  1. Stealthy\t\t\tMakes one HTTP request per target and also
  \t\t\t\tfollows redirects.
  3. Aggressive\t\t\tIf a level 1 plugin is matched, additional
  \t\t\t\trequests will be made.
  4. Heavy\t\t\tMakes a lot of HTTP requests per target. URLs
  \t\t\t\tfrom all plugins are attempted.

HTTP OPTIONS:
  --user-agent, -U=AGENT\tIdentify as AGENT instead of WhatWeb/#{WhatWeb::VERSION}.
  --header, -H\t\t\tAdd an HTTP header. eg "Foo:Bar". Specifying a
\t\t\t\tdefault header will replace it. Specifying an
\t\t\t\tempty value, e.g. "User-Agent:" will remove it.
  --follow-redirect=WHEN\tControl when to follow redirects. WHEN may be
\t\t\t\t`never', `http-only', `meta-only', `same-site',
\t\t\t\tor `always'. Default: #{$FOLLOW_REDIRECT}.
  --max-redirects=NUM\t\tMaximum number of redirects. Default: 10.

AUTHENTICATION:
  --user, -u=<user:password>\tHTTP basic authentication.
  --cookie, -c=COOKIES\t\tUse cookies, e.g. 'name=value; name2=value2'.
  --cookie-jar=FILE\t\tRead cookies from a file and save cookies to the
\t\t\t\tsame file. Creates the file if it doesn't exist.
  --no-cookies\t\t\tDisable automatic cookie handling (improves performance with high thread counts).

PROXY:
  --proxy\t\t\t<hostname[:port]> Set proxy hostname and port.
\t\t\t\tDefault: #{$PROXY_PORT}.
  --proxy-user\t\t\t<username:password> Set proxy user and password.

PLUGINS:
  --list-plugins, -l\t\tList all plugins.
  --info-plugins, -I=[SEARCH]\tList all plugins with detailed information.
\t\t\t\tOptionally search with keywords in a comma
\t\t\t\tdelimited list.
  --search-plugins=STRING\tSearch plugins for a keyword.
  --plugins, -p=LIST\t\tSelect plugins. LIST is a comma delimited set
\t\t\t\tof selected plugins. Default is all.
\t\t\t\tEach element can be a directory, file or plugin
\t\t\t\tname and can optionally have a modifier, +/-.
\t\t\t\tExamples: +/tmp/moo.rb,+/tmp/foo.rb
\t\t\t\ttitle,md5,+./plugins-disabled/
\t\t\t\t./plugins-disabled,-md5
\t\t\t\t-p + is a shortcut for -p +plugins-disabled.
  --grep, -g=STRING|REGEXP\tSearch for STRING or a Regular Expression. Shows
\t\t\t\tonly the results that match.
\t\t\t\tExamples: --grep "hello"
\t\t\t\t--grep "/he[l]*o/"
  --custom-plugin=DEFINITION\tDefine a custom plugin named Custom-Plugin,
\t\t\t\tExamples: ":text=>'powered by abc'"
\t\t\t\t":version=>/powered[ ]?by ab[0-9]/"
\t\t\t\t":ghdb=>'intitle:abc \\"powered by abc\\"'"
\t\t\t\t":md5=>'8666257030b94d3bdb46e05945f60b42'"
\t\t\t\t"{:text=>'powered by abc'}"
  --dorks=PLUGIN\t\tList Google dorks for the selected plugin.

OUTPUT:
  --verbose, -v\t\t\tVerbose output includes plugin descriptions.
\t\t\t\tUse twice for debugging.
  --colour,--color=WHEN\t\tcontrol whether colour is used. WHEN may be
\t\t\t\t`never', `always', or `auto'.
  --quiet, -q\t\t\tDo not display brief logging to STDOUT.
  --no-errors\t\t\tSuppress error messages.

LOGGING:
  --log-brief=FILE\t\tLog brief, one-line output.
  --log-verbose=FILE\t\tLog verbose output.
  --log-errors=FILE\t\tLog errors.
  --log-xml=FILE\t\tLog XML format.
  --log-json=FILE\t\tLog JSON format.
  --log-sql=FILE\t\tLog SQL INSERT statements.
  --log-sql-create=FILE\t\tCreate SQL database tables.
  --log-json-verbose=FILE\tLog JSON Verbose format.
  --log-magictree=FILE\t\tLog MagicTree XML format.
  --log-object=FILE\t\tLog Ruby object inspection format.
  --log-mongo-database\t\tName of the MongoDB database.
  --log-mongo-collection\tName of the MongoDB collection.
\t\t\t\tDefault: whatweb.
  --log-mongo-host\t\tMongoDB hostname or IP address.
\t\t\t\tDefault: 0.0.0.0.
  --log-mongo-username\t\tMongoDB username. Default: nil.
  --log-mongo-password\t\tMongoDB password. Default: nil.
  --log-elastic-index\t\tName of the index to store results. Default: whatweb
  --log-elastic-host\t\tHost:port of the elastic http interface. Default: 127.0.0.1:9200

PERFORMANCE & STABILITY:
  --max-threads, -t\t\tNumber of simultaneous threads. Default: 25.
  --open-timeout\t\tTime in seconds. Default: #{$HTTP_OPEN_TIMEOUT}.
  --read-timeout\t\tTime in seconds. Default: #{$HTTP_READ_TIMEOUT}.
  --wait=SECONDS\t\tWait SECONDS between connections.
\t\t\t\tThis is useful when using a single thread.
  --output-sync\t\t\tForce immediate output flushing for real-time
\t\t\t\tmonitoring (slower with high thread counts).
  --output-buffer-size=SIZE\tSet output buffer size. 0=unbuffered,
\t\t\t\tdefault=auto based on thread count.

HELP & MISCELLANEOUS:
  --short-help\t\t\tShort usage help.
  --help, -h\t\t\tComplete usage help.
  --debug\t\t\tRaise errors in plugins.
  --version\t\t\tDisplay version information.

EXAMPLE USAGE:
* Scan example.com.
  ./whatweb example.com

* Scan reddit.com slashdot.org with verbose plugin descriptions.
  ./whatweb -v reddit.com slashdot.org

* An aggressive scan of wired.com detects the exact version of WordPress.
  ./whatweb -a 3 www.wired.com

* Scan the local network quickly and suppress errors.
  whatweb --no-errors 192.168.0.0/24

* Scan the local network for https websites.
  whatweb --no-errors --url-prefix https:// 192.168.0.0/24

* Scan for crossdomain policies in the Alexa Top 1000.
  ./whatweb -i plugin-development/alexa-top-100.txt \\
  --url-suffix /crossdomain.xml -p crossdomain_xml

EOT
=begin
  suggestions = ''
  suggestions << "To enable MongoDB logging install the mongo gem.\n" unless gem_available?('mongo')
  suggestions << "To enable character set detection and MongoDB logging install the rchardet gem.\n" unless gem_available?('rchardet')

  unless suggestions.empty?
    puts
    puts 'OPTIONAL DEPENDENCIES'
    puts '-' * 80
    puts suggestions
    puts
  end
=end
end

#
# Command-line usage information (short)
#
def usage_short
  puts blue(WhatWeb::BANNER)

  puts(<<-EOT)
WhatWeb - Next generation web scanner version #{WhatWeb::VERSION}.
Developed by Andrew Horton (urbanadventurer) and Brendan Coles (bcoles)
Homepage: https://morningstarsecurity.com/research/whatweb

Usage: whatweb [options] <URLs>

  <TARGETs>\t\t\tEnter URLs, hostnames, IP addresses, filenames or
  \t\t\t\tIP ranges in CIDR, x.x.x-x, or x.x.x.x-x.x.x.x
  \t\t\t\tformat.
  --input-file=FILE, -i\t\tRead targets from a file.

  --aggression, -a=LEVEL\tSet the aggression level. Default: 1.
  1. Stealthy\t\t\tMakes one HTTP request per target and also
  \t\t\t\tfollows redirects.
  3. Aggressive\t\t\tIf a level 1 plugin is matched, additional
  \t\t\t\trequests will be made.

  --list-plugins, -l\t\tList all plugins.
  --info-plugins, -I=[SEARCH]\tList all plugins with detailed information.
\t\t\t\tOptionally search with a keyword.

  --verbose, -v\t\t\tVerbose output includes plugin descriptions.

Note: This is the short usage help. For the complete usage help use -h or --help.

EOT
end

if ARGV.empty? # faster usage info
  usage_short
  exit
end

### logging
$semaphore = Mutex.new
# this is noisy in debug
def $semaphore.reentrant_synchronize(&work)
  # This test is noisy in debug, shame there isn't a good way to test for mutex ownership
  $semaphore.lock
  $semaphore.unlock
rescue ThreadError
  raise if $WWDEBUG
  work.call
else
  $semaphore.synchronize(&work)
end

# Load message handling functions
require_relative 'lib/messages'

#
# Check for cookie performance warnings based on thread count
#
def check_cookie_performance_warning(max_threads)
  return if $NO_COOKIES
  
  case max_threads
  when 0..50
    # No warning - should work fine
  when 51..100
    notice "💡 Notice: With #{max_threads} threads, consider --no-cookies if scan performance is slow"
    
  when 101..200
    warning "⚠️  Performance Warning:"
    warning "   High thread count (#{max_threads}) may cause cookie jar bottlenecks"
    warning "   For optimal performance: whatweb --no-cookies [your other options]"
    
  when 201..Float::INFINITY
    error "🚨 Performance Alert:"
    error "   Very high thread count (#{max_threads}) will cause severe cookie contention"
    error "   STRONGLY RECOMMENDED: Add --no-cookies to prevent major slowdowns"
    error "   Command: whatweb --max-threads #{max_threads} --no-cookies [other options]"
  end
end

#
# Default options
#

# Plugins
plugin_selection = nil
use_custom_plugin = false
use_custom_grep_plugin = false

# Targets
input_file = nil

# Output
logging_list = []
mongo = {}
mongo[:use_mongo_log] = false
elastic = {}
elastic[:use_elastic_log] = false

# Target modification
url_prefix = ''
url_suffix = ''
url_pattern = nil

# HTTP connection
max_threads = 25
max_redirects = 10

# Cookie jar file path
cookie_jar_file = nil

# optional arguments work badly with URLs
opts = GetoptLong.new(
  ['-h', '--help', GetoptLong::NO_ARGUMENT],
  ['--short-help', GetoptLong::NO_ARGUMENT],
  ['-v', '--verbose', GetoptLong::NO_ARGUMENT],
  ['-l', '--list-plugins', GetoptLong::NO_ARGUMENT],
  ['-p', '--plugins', GetoptLong::REQUIRED_ARGUMENT],
  ['-I', '--info-plugins', '--search-plugins', GetoptLong::OPTIONAL_ARGUMENT],
  ['--dorks', GetoptLong::REQUIRED_ARGUMENT],
  ['--colour', '--color', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-object', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-brief', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-xml', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-json', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-json-verbose', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-magictree', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-verbose', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-mongo-collection', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-mongo-host', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-mongo-database', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-mongo-username', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-mongo-password', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-elastic-index', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-elastic-host', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-sql', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-sql-create', GetoptLong::REQUIRED_ARGUMENT],
  ['--log-errors', GetoptLong::REQUIRED_ARGUMENT],
  ['--no-errors', GetoptLong::NO_ARGUMENT],
  ['-i', '--input-file', GetoptLong::REQUIRED_ARGUMENT],
  ['-U', '--user-agent', GetoptLong::REQUIRED_ARGUMENT],
  ['-a', '--aggression', GetoptLong::REQUIRED_ARGUMENT],
  ['-t', '--max-threads', GetoptLong::REQUIRED_ARGUMENT],
  ['--follow-redirect', GetoptLong::REQUIRED_ARGUMENT],
  ['--max-redirects', GetoptLong::REQUIRED_ARGUMENT],
  ['--proxy', GetoptLong::REQUIRED_ARGUMENT],
  ['--proxy-user', GetoptLong::REQUIRED_ARGUMENT],
  ['--url-prefix', GetoptLong::REQUIRED_ARGUMENT],
  ['--url-suffix', GetoptLong::REQUIRED_ARGUMENT],
  ['--url-pattern', GetoptLong::REQUIRED_ARGUMENT],
  ['--custom-plugin', GetoptLong::REQUIRED_ARGUMENT],
  ['-g', '--grep', GetoptLong::REQUIRED_ARGUMENT],
  ['--open-timeout', GetoptLong::REQUIRED_ARGUMENT],
  ['--read-timeout', GetoptLong::REQUIRED_ARGUMENT],
  ['--header', '-H', GetoptLong::REQUIRED_ARGUMENT],
  ['--cookie', '-c', GetoptLong::REQUIRED_ARGUMENT],
  ['--cookie-jar', GetoptLong::REQUIRED_ARGUMENT],
  ['--no-cookies', GetoptLong::NO_ARGUMENT],
  ['--user', '-u', GetoptLong::REQUIRED_ARGUMENT],
  ['--wait', GetoptLong::REQUIRED_ARGUMENT],
  ['--debug', GetoptLong::NO_ARGUMENT],
  ['--version', GetoptLong::NO_ARGUMENT],
  ['-q', '--quiet', GetoptLong::NO_ARGUMENT],
  ['--output-sync', GetoptLong::NO_ARGUMENT],
  ['--output-buffer-size', GetoptLong::REQUIRED_ARGUMENT]
)

begin
  opts.each do |opt, arg|
    case opt
    when '-i', '--input-file'
      input_file = arg
    when '-l', '--list-plugins'
      PluginSupport.load_plugins
      PluginSupport.plugin_list
      exit
    when '-p', '--plugins'
      plugin_selection = arg
    when '-I', '--info-plugins'
      PluginSupport.load_plugins
      PluginSupport.plugin_info(arg.split(','))
      exit
    when '--dorks'
      PluginSupport.load_plugins
      PluginSupport.plugin_dorks(arg)
      exit

    when '--color', '--colour'
      $use_colour = 'always' unless arg # no argument
      case arg.downcase
      when 'auto'
        $use_colour = 'auto'
      when 'always'
        $use_colour = 'always'
      when 'never'
        $use_colour = false
      else
        raise('--colour argument not recognized')
      end
    when '--log-object'
      logging_list << LoggingObject.new(arg)
    when '--log-brief'
      logging_list << LoggingBrief.new(arg)
    when '--log-xml'
      logging_list << LoggingXML.new(arg)
    when '--log-magictree'
      logging_list << LoggingMagicTreeXML.new(arg)
    when '--log-verbose'
      logging_list << LoggingVerbose.new(arg)
    when '--log-sql'
      logging_list << LoggingSQL.new(arg)
    when '--log-sql-create'
      PluginSupport.load_plugins('+')
      # delete the file if it already exists
      begin
        File.delete(arg)
      rescue
        warning("File #{arg} already exists and cannot be removed")
      end
      LoggingSQL.new(arg).create_tables
      notice("SQL CREATE statements written to #{arg}")
      exit
    when '--log-json'
      logging_list << LoggingJSON.new(arg)
    when '--log-json-verbose'
      logging_list << LoggingJSONVerbose.new(arg)
    when '--log-mongo-collection'
      unless defined?(Mongo) && defined?(CharDet)
        raise "Mongo logging requires the following gems to be installed.\n - mongo\n - rchardet\n"
      end

      mongo[:collection] = arg
      mongo[:use_mongo_log] = true
    when '--log-mongo-host'
      unless defined?(Mongo) && defined?(CharDet)
        raise "Mongo logging requires the following gems to be installed.\n - mongo\n - rchardet\n"
      end

      mongo[:host] = arg
      mongo[:use_mongo_log] = true
    when '--log-mongo-database'
      unless defined?(Mongo) && defined?(CharDet)
        raise "Mongo logging requires the following gems to be installed.\n - mongo\n - rchardet\n"
      end

      mongo[:database] = arg
      mongo[:use_mongo_log] = true
    when '--log-mongo-username'
      unless defined?(Mongo) && defined?(CharDet)
        raise "Mongo logging requires the following gems to be installed.\n - mongo\n - rchardet\n"
      end

      mongo[:username] = arg
      mongo[:use_mongo_log] = true
    when '--log-mongo-password'
      unless defined?(Mongo) && defined?(CharDet)
        raise "Mongo logging requires the following gems to be installed.\n - mongo\n - rchardet\n"
      end

      mongo[:password] = arg
      mongo[:use_mongo_log] = true
    when '--log-elastic-index'
      elastic[:index] = arg
      elastic[:use_elastic_log] = true
    when '--log-elastic-host'
      elastic[:host] = arg
      elastic[:use_elastic_log] = true
    when '--log-errors'
      $LOG_ERRORS = LoggingErrors.new(arg)
    when '--no-errors'
      $NO_ERRORS = true
    when '-U', '--user-agent'
      $USER_AGENT = arg
    when '-t', '--max-threads'
      max_threads = arg.to_i
    when '-a', '--aggression'
      raise "Agression level must be 1,3, or 4. #{arg} is invalid." unless [1, 3, 4].include? arg.to_i

      $AGGRESSION = arg.to_i
    when '--proxy'
      $USE_PROXY = true
      $PROXY_HOST = arg.to_s.split(':')[0]
      $PROXY_PORT = arg.to_s.split(':')[1].to_i if arg.to_s.include?(':')
    when '--proxy-user'
      $PROXY_USER = arg.to_s.split(':')[0]
      $PROXY_PASS = arg.to_s.scan(/^[^:]*:(.+)/).to_s if arg =~ /^[^:]*:(.+)/
    when '-q', '--quiet'
      $QUIET = true
    when '--url-prefix'
      url_prefix = arg
    when '--url-suffix'
      url_suffix = arg
    when '--url-pattern'
      url_pattern = arg
    when '--custom-plugin'
      use_custom_plugin = true if PluginSupport.custom_plugin(arg)
    when '--grep', '-g'
      use_custom_grep_plugin = true if PluginSupport.custom_plugin(arg, 'grep')
    when '--follow-redirect'
      raise('Invalid --follow-redirect parameter.') unless %w[never http-only meta-only same-site always].include?(arg.downcase)

      $FOLLOW_REDIRECT = arg.downcase
    when '--max-redirects'
      max_redirects = arg.to_i
    when '--open-timeout'
      $HTTP_OPEN_TIMEOUT = arg.to_i
    when '--read-timeout'
      $HTTP_READ_TIMEOUT = arg.to_i
    when '--wait'
      $WAIT = arg.to_i
    when '-H', '--header'
      begin
        x = arg.scan(/([^:]+):(.*)/).flatten
        raise if x.empty?

        $CUSTOM_HEADERS[x.first] = x.last
      rescue
        raise('Invalid --header parameter.')
      end
    when '-c', '--cookie'
      begin
        raise if arg.empty?

        $CUSTOM_HEADERS['Cookie'] = arg
      rescue
        raise('Cookie require a parameter, e.g. name=value; name2=value2')
      end
    when '--cookie-jar'
      cookie_jar_file = arg
      begin
        if File.readable?(arg)
          # Check if this is legacy format (single domain) for backward compatibility
          lines = File.readlines(arg).map(&:strip)
          
          # If it's NOT the new domain-aware format, load as legacy cookies
          unless lines.any? { |line| line.start_with?('# Domain:') }
            # Legacy format - load into $CUSTOM_HEADERS for backward compatibility
            cookie_lines = lines.delete_if { |line| line =~ /^$/ }
            unless cookie_lines.empty?
              $CUSTOM_HEADERS['Cookie'] = cookie_lines.map { |line| line.sub(/([^;])$/, '\1;') }.join(' ')
            end
          end
          # Note: Domain-aware format is handled automatically by SimpleCookieJar.load_from_file
        else
          # File doesn't exist yet - that's OK, we'll create it when saving
          # Just store the path for later use
        end
      rescue
        raise("Could not read cookies from #{arg}.")
      end
    when '--no-cookies'
      $NO_COOKIES = true
    when '-u', '--user'
      unless arg.include?(':')
        raise("Incorrect credentials format.")
      end

      $BASIC_AUTH_USER, $BASIC_AUTH_PASS = arg.split(':', 2)
    when '--debug'
      $WWDEBUG = true
    when '--short-help'
      usage_short
      exit
    when '-h', '--help'
      usage_full
      exit
    when '-v', '--verbose'
      $verbose = $verbose + 1
    when '--version'
      puts "WhatWeb version #{WhatWeb::VERSION} ( https://morningstarsecurity.com/research/whatweb/ )"
      exit
    when '--output-sync'
      $OUTPUT_SYNC = true
    when '--output-buffer-size'
      $OUTPUT_BUFFER_SIZE = arg.to_i
    end
  end
rescue Errno::EPIPE
  exit
rescue StandardError, GetoptLong::Error => e
  # Disable colours in Windows environments for errors in usage
  $use_colour = false if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
  error("Error in processing commandline options - #{e}")
  error e.backtrace if $WWDEBUG
  exit
end

# sanity check # Disable colours in Windows environments when set to auto
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
  $use_colour = false unless $use_colour == 'always'
end

### PLUGINS
plugin_selection += ',+Custom-Plugin'	if use_custom_plugin && plugin_selection
plugin_selection += ',+Grep'	if use_custom_grep_plugin && plugin_selection
plugins = PluginSupport.load_plugins(plugin_selection)
# load all the plugins

# sanity check # no plugins?
if plugins.empty?
  error 'No plugins selected, exiting.'
  exit 1
end

# optimise plugins
PluginSupport.precompile_regular_expressions

# Store thread count globally for logging classes (before creating loggers)
$THREADS = max_threads

# Configure smart defaults for output performance based on thread count
unless defined?($OUTPUT_SYNC)
  $OUTPUT_SYNC = (max_threads <= 5)  # Sync for low thread counts
end

unless defined?($OUTPUT_BUFFER_SIZE)
  $OUTPUT_BUFFER_SIZE = case max_threads
                        when 1..5 then 1      # No buffering for low threads
                        when 6..50 then 10    # Small buffer for medium (includes default 25)
                        else 25               # Larger buffer for high threads (51+)
                        end
end

### OUTPUT
logging_list << LoggingBrief.new(STDOUT, sync: $OUTPUT_SYNC) unless $QUIET || $verbose > 0 # by default logging brief
logging_list << LoggingObject.new if $verbose > 1 # full logging if -vv
logging_list << LoggingVerbose.new(STDOUT, sync: $OUTPUT_SYNC) if $verbose > 0 # full logging if -v

## logging dependencies
if mongo[:use_mongo_log]
  unless plugins.map { |a, _b| a }.include?('Charset')
    error('MongoDB logging requires the Charset plugin to be activated. The Charset plugin is the slowest whatweb plugin, it not included by default, and resides in the plugins-disabled folder. Use ./whatweb -p +./plugins-disabled/charset.rb to enable it.')
    exit 1
  end

  logging_list << LoggingMongo.new(mongo)
end

logging_list << LoggingElastic.new(elastic) if elastic[:use_elastic_log]

## Headers
$CUSTOM_HEADERS['User-Agent'] = $USER_AGENT unless $CUSTOM_HEADERS['User-Agent']
$CUSTOM_HEADERS.delete_if { |_k, v| v == '' }

## Cookie Jar Initialization
unless $NO_COOKIES
  require_relative 'lib/simple_cookie_jar'
  $COOKIE_JAR = SimpleCookieJar.new(max_domains: 10_000, cookie_jar_file: cookie_jar_file)
  
  # Warn about performance implications
  check_cookie_performance_warning(max_threads)
end

urls = ARGV

# Initialize Scanner
begin
  scanner = WhatWeb::Scan.new(
    # Targets
    urls,
    input_file: input_file,

    # HTTP connection
    max_threads: max_threads,

    # Target modification
    url_prefix: url_prefix,
    url_suffix: url_suffix,
    url_pattern: url_pattern
  )
rescue SystemExit, Interrupt
  error 'Interrupted. Exiting...'
  exit 1
rescue => e
  error(e.message)
  error(e.backtrace.join("\n")) if $WWDEBUG
  exit 1
end

# run scan
scanner.scan do |target|
  # TODO: change so we can call it without Target object and with just Headers, HTML, etc.
  # note this modifies target with redirection info
  result = WhatWeb::Parser.run_plugins(target, plugins, scanner: scanner)

  # check for redirection
  WhatWeb::Redirect.new(target, scanner, max_redirects)

  WhatWeb::Parser.parse(
    target,
    result,
    logging_list: logging_list,
    grep_plugin: use_custom_grep_plugin
  )
end

# Save cookies to jar file if specified
if defined?($COOKIE_JAR) && $COOKIE_JAR
  $COOKIE_JAR.save_to_file
end

# close down logging and plugins
logging_list.each(&:close)
Plugin.shutdown_all

# Display cookie jar statistics if enabled and cookies were used
if defined?($COOKIE_JAR) && $COOKIE_JAR && (!defined?($NO_COOKIES) || !$NO_COOKIES) && $verbose && $verbose > 1
  stats = $COOKIE_JAR.stats
  notice "Cookie jar statistics:"
  notice "  Domains: #{stats[:domains]}/#{stats[:max_domains]}"
  notice "  Requests processed: #{stats[:requests_processed]}"
  notice "  Memory estimate: #{stats[:memory_estimate_kb]}KB"
  
  # Show actual cookies in debug mode
  $COOKIE_JAR.display_cookies if $verbose > 2
end

# pp $PLUGIN_TIMES.sort_by {|x,y|y }
