#
# Copyright (c) 2006-2025 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#

module BeEF
  module Core
    module Rest
      class Modules < BeEF::Core::Router::Router
        config = BeEF::Core::Configuration.instance

        before do
          error 401 unless params[:token] == config.get('beef.api_token')
          halt 401 unless BeEF::Core::Rest.permitted_source?(request.ip)
          headers 'Content-Type' => 'application/json; charset=UTF-8',
                  'Pragma' => 'no-cache',
                  'Cache-Control' => 'no-cache',
                  'Expires' => '0'
        end

        #
        # @note Get all available and enabled modules (id, name, category)
        #
        get '/' do
          mods = BeEF::Core::Models::CommandModule.all

          mods_array = []

          mods.each do |mod|
            modk = BeEF::Module.get_key_by_database_id(mod.id)
            next unless BeEF::Module.is_enabled(modk)

            mods_array << {
              'id' => mod.id,
              'class' => config.get("beef.module.#{modk}.class"),
              'name' => config.get("beef.module.#{modk}.name"),
              'category' => config.get("beef.module.#{modk}.category")
            }
          end
          mods_array.to_json
        end

        get '/search/:mod_name' do
          mod = BeEF::Core::Models::CommandModule.where(name: params[:mod_name]).first
          result = {}
          result = { 'id' => mod.id } unless mod.nil?
          result.to_json
        end

        #
        # @note Get the module definition (info, options)
        #
        get '/:mod_id' do
          cmd = BeEF::Core::Models::CommandModule.find(params[:mod_id])
          error 404 if cmd.nil?
          modk = BeEF::Module.get_key_by_database_id(params[:mod_id])
          error 404 if modk.nil?

          # TODO: check if it's possible to also retrieve the TARGETS supported
          {
            'name' => cmd.name,
            'description' => config.get("beef.module.#{cmd.name}.description"),
            'category' => config.get("beef.module.#{cmd.name}.category"),
            'options' => BeEF::Module.get_options(modk) # TODO: => get also payload options..get_payload_options(modk,text)
          }.to_json
        end

        # @note Get the module result for the specific executed command
        #
        # Example with the Alert Dialog
        # GET /api/modules/wiJCKAJybcB6aXZZOj31UmQKhbKXY63aNBeODl9kvkIuYLmYTooeGeRD7Xn39x8zOChcUReM3Bt7K0xj/86/1?token=0a931a461d08b86bfee40df987aad7e9cfdeb050 HTTP/1.1
        # Host: 127.0.0.1:3000
        #===response (snip)===
        # HTTP/1.1 200 OK
        # Content-Type: application/json; charset=UTF-8
        #
        # {"date":"1331637093","data":"{\"data\":\"text=michele\"}"}
        #
        get '/:session/:mod_id/:cmd_id' do
          hb = BeEF::Core::Models::HookedBrowser.where(session: params[:session]).first
          error 401 if hb.nil?
          cmd = BeEF::Core::Models::Command.where(hooked_browser_id: hb.id,
                                                  command_module_id: params[:mod_id], id: params[:cmd_id]).first
          error 404 if cmd.nil?
          results = BeEF::Core::Models::Result.where(hooked_browser_id: hb.id, command_id: cmd.id)
          error 404 if results.nil?

          results_hash = {}
          i = 0
          results.each do |result|
            results_hash[i] = {
              'date' => result.date,
              'data' => result.data
            }
            i += 1
          end
          results_hash.to_json
        end

        #
        # @note Fire a new command module to the specified hooked browser.
        # Return the command_id of the executed module if it has been fired correctly.
        # Input must be specified in JSON format
        #
        # +++ Example with the Alert Dialog: +++
        # POST /api/modules/wiJCKAJybcB6aXZZOj31UmQKhbKXY63aNBeODl9kvkIuYLmYTooeGeRD7Xn39x8zOChcUReM3Bt7K0xj/86?token=5b17be64715a184d66e563ec9355ee758912a61d HTTP/1.1
        # Host: 127.0.0.1:3000
        # Content-Type: application/json; charset=UTF-8
        # Content-Length: 18
        #
        # {"text":"michele"}
        #===response (snip)===
        # HTTP/1.1 200 OK
        # Content-Type: application/json; charset=UTF-8
        # Content-Length: 35
        #
        # {"success":"true","command_id":"1"}
        #
        # +++ Example with a Metasploit module (Adobe FlateDecode Stream Predictor 02 Integer Overflow) +++
        # +++ note that in this case we cannot query BeEF/Metasploit if module execution was successful or not.
        # +++ this is why there is "command_id":"not_available" in the response
        # POST /api/modules/wiJCKAJybcB6aXZZOj31UmQKhbKXY63aNBeODl9kvkIuYLmYTooeGeRD7Xn39x8zOChcUReM3Bt7K0xj/236?token=83f13036060fd7d92440432dd9a9b5e5648f8d75 HTTP/1.1
        # Host: 127.0.0.1:3000
        # Content-Type: application/json; charset=UTF-8
        # Content-Length: 81
        #
        # {"SRVPORT":"3992", "URIPATH":"77345345345dg", "PAYLOAD":"generic/shell_bind_tcp"}
        #===response (snip)===
        # HTTP/1.1 200 OK
        # Content-Type: application/json; charset=UTF-8
        # Content-Length: 35
        #
        # {"success":"true","command_id":"not_available"}
        #
        post '/:session/:mod_id' do
          hb = BeEF::Core::Models::HookedBrowser.where(session: params[:session]).first
          error 401 if hb.nil?
          modk = BeEF::Module.get_key_by_database_id(params[:mod_id])
          error 404 if modk.nil?

          request.body.rewind
          begin
            data = JSON.parse request.body.read
            options = []
            data.each { |k, v| options.push({ 'name' => k, 'value' => v }) }
            exec_results = BeEF::Module.execute(modk, params[:session], options)
            exec_results.nil? ? '{"success":"false"}' : '{"success":"true","command_id":"' + exec_results.to_s + '"}'
          rescue StandardError
            print_error "Invalid JSON input for module '#{params[:mod_id]}'"
            error 400 # Bad Request
          end
        end

        #
        # @note Fire a new command module to multiple hooked browsers.
        # Returns the command IDs of the launched module, or 0 if firing got issues.
        # Use "hb_ids":["ALL"] to run on all hooked browsers
        # Use "hb_ids":["ALL_ONLINE"] to run on all hooked browsers currently online
        #
        # POST request body example (for modules that don't need parameters, just remove "mod_params")
        #  {
        #    "mod_id":1,
        #    "mod_params":{
        #       "question":"are you hooked?"
        #     },
        #    "hb_ids":[1,2]
        #   }
        #
        # response example: {"1":16,"2":17}
        #
        # curl example (alert module with custom text, 2 hooked browsers)):
        #
        # curl -H "Content-Type: application/json; charset=UTF-8" -d '{"mod_id":110,"mod_params":{"text":"mucci?"},"hb_ids":[1,2]}'
        #-X POST http://127.0.0.1:3000/api/modules/multi_browser?token=2316d82702b83a293e2d46a0886a003a6be0a633
        #
        post '/multi_browser' do
          request.body.rewind
          begin
            body = JSON.parse request.body.read

            modk = BeEF::Module.get_key_by_database_id body['mod_id']
            error 404 if modk.nil?
            mod_params = []

            unless body['mod_params'].nil?
              body['mod_params'].each do |k, v|
                mod_params.push({ 'name' => k, 'value' => v })
              end
            end

            hb_ids = body['hb_ids']
            results = {}

            # run on all hooked browsers currently online?
            if hb_ids.first =~ /\Aall_online\z/i
              hb_ids = []
              BeEF::Core::Models::HookedBrowser.where(
                :lastseen.gte => (Time.new.to_i - 15)
              ).each { |hb| hb_ids << hb.id }
            # run on all hooked browsers?
            elsif hb_ids.first =~ /\Aall\z/i
              hb_ids = []
              BeEF::Core::Models::HookedBrowser.all.each { |hb| hb_ids << hb.id }
            end

            # run modules
            hb_ids.each do |hb_id|
              hb = BeEF::Core::Models::HookedBrowser.find(hb_id)
              if hb.nil?
                results[hb_id] = 0
                next
              else
                cmd_id = BeEF::Module.execute(modk, hb.session, mod_params)
                results[hb_id] = cmd_id
              end
            end
            results.to_json
          rescue StandardError
            print_error 'Invalid JSON input passed to endpoint /api/modules/multi_browser'
            error 400 # Bad Request
          end
        end

        # @note Fire multiple command modules to a single hooked browser.
        # Returns the command IDs of the launched modules, or 0 if firing got issues.
        #
        # POST request body example (for modules that don't need parameters, just pass an empty JSON object like {} )
        # { "hb":"vkIwVV3ok5i5vH2f8sxlkoaKqAGKCbZXdWqE9vkHNFBhI8aBBHvtZAGRO2XqFZXxThBlmKlRiVwPeAzj",
        #    "modules": [
        #        {    # test_return_long_string module with custom input
        #             "mod_id":99,
        #              "mod_input":[{"repeat":"10"},{"repeat_string":"ABCDE"}]
        #        },
        #        {   # prompt_dialog module with custom input
        #            "mod_id":116,
        #            "mod_input":[{"question":"hooked?"}]
        #        },
        #        {   # alert_dialog module without input (using default input, if any)
        #            "mod_id":128,
        #            "mod_input":[]
        #        }
        #    ]
        #  }
        # response example: {"99":7,"116":8,"128":0} # <- This means the alert_dialog had issues (see return value 0)
        #
        # curl example (test_return_long_string and prompt_dialog module with custom inputs)):
        #
        # curl -H "Content-Type: application/json; charset=UTF-8" -d '{"hb":"vkIwVV3ok5i5vH2f8sxlkoaKqAGKCbZXdWqE9vkHNFBhI8aBBHvtZAGRO2XqFZXxThBlmKlRiVwPeAzj",
        # "modules":[{"mod_id":99,"mod_input":[{"repeat":"10"},{"repeat_string":"ABCDE"}]},{"mod_id":116,"mod_input":[{"question":"hooked?"}]},{"mod_id":128,"mod_input":[]}]}'
        #  -X POST http://127.0.0.1:3000/api/modules/multi_module?token=e640483ae9bca2eb904f003f27dd4bc83936eb92
        #
        post '/multi_module' do
          request.body.rewind
          begin
            body = JSON.parse request.body.read
            hb = BeEF::Core::Models::HookedBrowser.where(session: body['hb']).first
            error 401 if hb.nil?

            results = {}
            unless body['modules'].nil?
              body['modules'].each do |mod|
                mod_id = mod['mod_id']
                mod_k = BeEF::Module.get_key_by_database_id mod['mod_id']
                if mod_k.nil?
                  results[mod_id] = 0
                  next
                else
                  mod_params = []
                  mod['mod_input'].each do |input|
                    input.each do |k, v|
                      mod_params.push({ 'name' => k, 'value' => v })
                    end
                  end
                  cmd_id = BeEF::Module.execute(mod_k, hb.session, mod_params)
                  results[mod_id] = cmd_id
                end
              end
            end
            results.to_json
          rescue StandardError
            print_error 'Invalid JSON input passed to endpoint /api/modules/multi'
            error 400 # Bad Request
          end
        end
      end
    end
  end
end
