# 3rd party gems
require 'http/cookie_jar/hash_store'
require 'http/cookie_jar'
require 'http/cookie'

# This class is a collection of Http Cookies with some built in convenience methods.
# Acts as a wrapper for the +::HTTP::CookieJar+ (https://www.rubydoc.info/gems/http-cookie/1.0.2/HTTP/CookieJar) class.

module Msf
  class Exploit
    class Remote
      module HTTP
        # Acts as a wrapper for the 3rd party CookieJar (http-cookie)
        class HttpCookieJar

          # Returns a new instance of +HttpCookieJar+.
          def initialize
            @cookie_jar = ::HTTP::CookieJar.new({
              store: HashStoreWithoutAutomaticExpiration
            })
          end

          # Adds +cookie+ to the jar.
          #
          # +cookie+ must be an instance or subclass of +Msf::Exploit::Remote::HTTP::HttpCookie+, or a `TypeError`
          # will be raised.
          #
          # Returns +self+.
          def add(cookie)
            raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{Msf::Exploit::Remote::HTTP::HttpCookie}" unless cookie.is_a?(Msf::Exploit::Remote::HTTP::HttpCookie)

            @cookie_jar.add(cookie)
            self
          end

          # Will remove any cookie from the jar that has the same +name+, +domain+ and +path+ as the passed +cookie+.
          #
          # Returns +self+.
          def delete(cookie)
            return if @cookie_jar.cookies.empty?
            raise TypeError, "Passed cookie is of class '#{cookie.class}' and not a subclass of '#{Msf::Exploit::Remote::HTTP::HttpCookie}" unless cookie.is_a?(Msf::Exploit::Remote::HTTP::HttpCookie)

            @cookie_jar.delete(cookie)
            self
          end

          # Returns an unordered array of all cookies stored in the jar.
          def cookies
            @cookie_jar.cookies
          end

          # Will remove all cookies from the jar.
          #
          # Returns +nil+.
          def clear
            @cookie_jar.clear
            self
          end

          # Will remove all expired cookies. If +expire_all+ is set as true, all session cookies are removed as well.
          #
          # Returns +self+.
          def cleanup(expire_all = false)
            @cookie_jar.cleanup(expire_all)
            self
          end

          # Returns +true+ if the jar contains no cookies, else +false+.
          def empty?
            @cookie_jar.empty?
          end

          # Parses a Set-Cookie header value +set_cookie_header+ and returns an array of
          # +::Msf::Exploit::Remote::HTTP::HttpCookie+ objects. Parts (separated by commas) that are malformed or
          # considered unacceptable are silently ignored.
          def parse(set_cookie_header, origin_url)
            cookies = []
            ::HTTP::Cookie::Scanner.new(set_cookie_header).scan_set_cookie do |name, value, attrs|
              if name.nil? || name.empty?
                next
              end

              if attrs && attrs.is_a?(Hash)
                attrs = attrs.transform_keys(&:to_sym)
                attrs[:origin] = origin_url
                cookies << HttpCookie.new(name, value, **attrs)
              else
                raise ArgumentError, "Cookie header could not be parsed by 'scan_set_cookie' successfully."
              end
            end

            cookies
          end

          # Same as +parse+, but each +::Msf::Exploit::Remote::HTTP::HttpCookie+ is also added to the jar.
          def parse_and_merge(set_cookie_header, origin_url)
            cookies = parse(set_cookie_header, origin_url)
            cookies.each { |c| add(c) }
            cookies
          end

          # Modules are replicated before running. This method ensures that the cookie jar from
          # one run, will not impact subsequent runs.
          def initialize_copy(other)
            super
            @cookie_jar = other.instance_variable_get(:@cookie_jar).clone
          end
        end

        # On top of iterating over every item in the store, +::HTTP::CookieJar::HashStore+ also deletes any expired cookies
        # and has the option to filter cookies based on whether they are parent of a passed url.
        #
        # We've removed the extraneous features in the overwritten method.
        #   - The deletion of cookies while you're iterating over them complicated simple cookie management. It also
        #     prevented sending expired cookies if needed for an exploit
        #   - Any URL passed for filtering could be resolved to nil if it was improperly formed or resolved to a eTLD,
        #     which was too brittle for our uses
        class HashStoreWithoutAutomaticExpiration < ::HTTP::CookieJar::HashStore
          def each(uri = nil)
            raise ArgumentError, "HashStoreWithoutAutomaticExpiration.each doesn't support url filtering" if uri

            synchronize do
              @jar.each do |_domain, paths|
                paths.each do |_path, hash|
                  hash.each do |_name, cookie|
                    yield cookie
                  end
                end
              end
            end
            self
          end
        end
      end
    end
  end
end
