module Msf

module Exploit::Git::Lfs

  class Response < Rex::Proto::Http::Response

    include Msf::Exploit::Git::Lfs

    attr_reader :operation, :requested_oids, :valid_objs, :transfers, :base_addr

    STATUS_CODES =
    {
      200 => 'OK',
      401 => 'Auth required, but creds were not sent',
      403 => 'User does not have write access',
      404 => 'The object does not exist on the server',
      406 => 'The Accept header needs to be application/vnd.git-lfs+json',
      410 => 'The object was removed by owner of repository',
      413 => 'The batch API request contained too many objects',
      422 => 'Requested object(s) invalid',
      429 => 'Server rate limit reached',
      501 => 'The server has not implemented the current method',
      507 => 'The server has insufficient storage capacity to complete the request',
      509 => 'The bandwidth limit for the user or repository has been exceeded'
    }

    def initialize(opts = {})
      @uri = opts[:uri] || '/'
      @base_addr = opts[:base_addr] || ''
      @code = opts[:code] || 200
      @message = opts[:message] || STATUS_CODES[@code]
      @operation = opts[:operation] || 'download'
      @requested_oids = opts[:requested_oids] || []
      @valid_objs = opts[:valid_objs] || []
      @transfers = opts[:transfers] || [ 'basic' ]

      super(@code, @message)
      set_headers
    end

    def set_headers
      required_header = 'application/vnd.git-lfs+json'

      @headers['Accept'] = required_header
      @headers['Content-Type'] = required_header
    end

    # Checks that the instance's requested oids
    # are valid Git objects in the repo
    # 
    # @param [Array] list of objects in repo
    #
    # @return [Boolean]
    def valid_objects?(repo_objects)
      repo_objects = [ repo_objects ] unless repo_objects.kind_of?(Array)

      return false if repo_objects.empty?

      validate_requested_objects(repo_objects)
      @valid_objs.all? { |obj| @requested_oids.include?(self.class.obj_sha256(obj.content)) } 
    end

    # Populates the valid objects list based
    # on oids requested by the client
    #
    # @param [Array] list of objects in repo
    #
    def validate_requested_objects(repo_objects)
      repo_objects.each do |obj|
        @requested_oids.each do |oid|
          @valid_objs << obj if self.class.obj_sha256(obj.content) == oid
        end
      end

      @valid_objs.uniq!
    end

    def self.obj_sha256(content)
      Digest::SHA256.hexdigest(content)
    end

    def self.from_http_request(request, git_addr = '')
      return nil unless request && request.body

      opts = {}
      opts[:uri] = request.uri
      case request.method
      when 'GET'
        uri_parts = opts[:uri].split('/')
        requested_oid = uri_parts.last
        unless requested_oid
          opts[:code] = 422
          return Response.new(opts)
        end

        opts[:requested_oids] = [ requested_oid ]
        Response.new(opts)
      when 'POST'
        unless request.uri.include?('info/lfs/objects/batch')
          opts[:code] = 404
          opts[:message] = 'Not Found'
          return Response.new(opts)
        end

        git_addr = git_addr.to_s
        if git_addr.empty?
          raise ArgumentError, 'The URL for the Git repository was not supplied'
        end

        git_addr = git_addr.gsub(/\/\w+\.git/, '')
        opts[:base_addr] = git_addr
        parsed_options = parse_request_data(request.body)
        opts.merge!(parsed_options)
        Response.new(opts)
      end
    end

    def self.parse_request_data(body)
      json_data = JSON.parse(body)

      oids = []
      req_opts = {}
      req_opts[:operation] = json_data['operation'] || 'download'
      objects = json_data['objects'].nil? ? [] : json_data['objects']
      objects.each { |obj| oids << obj['oid'] }
      req_opts[:requested_oids] = oids
      req_opts[:transfers] = json_data['transfers']

      req_opts
    end
  end
end
end
