lib/kpm/nexus_helper/nexus_api_calls_v2.rb



# frozen_string_literal: true

require 'net/http'
require 'uri'
require 'rexml/document'
require 'openssl'

module KPM
  module NexusFacade
    class UnexpectedStatusCodeException < StandardError
      def initialize(code)
        @code = code
      end

      def message
        "The server responded with a #{@code} status code which is unexpected."
      end
    end

    class ArtifactMalformedException < StandardError
      class << self
        def message
          'Please submit your request using 4 colon-separated values. `groupId:artifactId:version:extension`'
        end
      end
    end

    # This is an extract and slim down of functions needed from nexus_cli to maintain the response expected by the base_artifact.
    class NexusApiCallsV2
      READ_TIMEOUT_DEFAULT = 60
      OPEN_TIMEOUT_DEFAULT = 60

      ERROR_MESSAGE_404 = 'The artifact you requested information for could not be found. Please ensure it exists inside the Nexus.'

      attr_reader :version, :configuration, :ssl_verify

      attr_accessor :logger

      def initialize(configuration, ssl_verify, logger)
        @configuration = configuration
        @ssl_verify = ssl_verify
        @logger = logger
      end

      def search_for_artifacts(coordinates)
        logger.debug "Entered - Search for artifact, coordinates: #{coordinates}"
        response = get_response(coordinates, search_for_artifact_endpoint(coordinates), %i[g a])

        case response.code
        when '200'
          logger.debug "response body: #{response.body}"
          response.body
        when '404'
          raise StandardError, ERROR_MESSAGE_404
        else
          raise UnexpectedStatusCodeException, response.code
        end
      end

      def get_artifact_info(coordinates)
        get_response_with_retries(coordinates, get_artifact_info_endpoint(coordinates), nil)
      end

      def pull_artifact(coordinates, destination)
        file_name = get_file_name(coordinates)
        destination = File.join(File.expand_path(destination || '.'), file_name)
        logger.debug { "Downloading to destination: #{destination}" }

        File.open(destination, 'wb') do |io|
          io.write(get_response_with_retries(coordinates, pull_artifact_endpoint(coordinates), nil))
        end

        {
          file_name: file_name,
          file_path: File.expand_path(destination),
          version: version,
          size: File.size(File.expand_path(destination))
        }
      end

      def pull_artifact_endpoint(_coordinates)
        '/service/local/artifact/maven/redirect'
      end

      def get_artifact_info_endpoint(_coordinates)
        '/service/local/artifact/maven/resolve'
      end

      def search_for_artifact_endpoint(_coordinates)
        '/service/local/lucene/search'
      end

      def build_query_params(coordinates, what_parameters = nil)
        artifact = parse_coordinates(coordinates)
        @version = artifact[:version].to_s

        query = { g: artifact[:group_id], a: artifact[:artifact_id], e: artifact[:extension], v: version, r: configuration[:repository] }
        query.merge!(c: artifact[:classifier]) unless artifact[:classifier].nil?

        params = what_parameters.nil? ? query : {}
        what_parameters.each { |key| params[key] = query[key] unless query[key].nil? } unless what_parameters.nil?

        params.map { |key, value| "#{key}=#{value}" }.join('&')
      end

      private

      def parse_coordinates(coordinates)
        raise ArtifactMalformedException if coordinates.nil?

        split_coordinates = coordinates.split(':')
        raise ArtifactMalformedException if split_coordinates.empty? || (split_coordinates.size > 5)

        artifact = {}

        artifact[:group_id] = split_coordinates[0]
        artifact[:artifact_id] = split_coordinates[1]
        artifact[:extension] = split_coordinates.size > 3 ? split_coordinates[2] : 'jar'
        artifact[:classifier] = split_coordinates.size > 4 ? split_coordinates[3] : nil
        artifact[:version] = split_coordinates[-1]

        artifact[:version].upcase! if version == 'latest'

        artifact
      end

      def get_file_name(coordinates)
        artifact = parse_coordinates(coordinates)

        artifact[:version] = REXML::Document.new(get_artifact_info(coordinates)).elements['//version'].text if artifact[:version].casecmp('latest')

        if artifact[:classifier].nil?
          "#{artifact[:artifact_id]}-#{artifact[:version]}.#{artifact[:extension]}"
        else
          "#{artifact[:artifact_id]}-#{artifact[:version]}-#{artifact[:classifier]}.#{artifact[:extension]}"
        end
      end

      def get_response_with_retries(coordinates, endpoint, what_parameters)
        logger.debug { "Fetching coordinates=#{coordinates}, endpoint=#{endpoint}, params=#{what_parameters}" }
        response = get_response(coordinates, endpoint, what_parameters)
        logger.debug { "Response body: #{response.body}" }
        process_response_with_retries(response)
      end

      def process_response_with_retries(response)
        case response.code
        when '200'
          response.body
        when '301', '302', '307'
          location = response['Location']
          logger.debug { "Following redirect to #{location}" }

          new_path = location.gsub!(configuration[:url], '')
          if new_path.nil?
            # Redirect to another domain (e.g. CDN)
            get_raw_response_with_retries(location)
          else
            get_response_with_retries(nil, location, nil)
          end
        when '404'
          raise StandardError, ERROR_MESSAGE_404
        else
          raise UnexpectedStatusCodeException, response.code
        end
      end

      def get_response(coordinates, endpoint, what_parameters)
        http = build_http
        query_params = build_query_params(coordinates, what_parameters) unless coordinates.nil?
        endpoint = endpoint_with_params(endpoint, query_params) unless coordinates.nil?
        request = Net::HTTP::Get.new(endpoint)
        if configuration.key?(:username) && configuration.key?(:password)
          request.basic_auth(configuration[:username], configuration[:password])
        elsif configuration.key?(:token)
          request['Authorization'] = "token #{configuration[:token]}"
        end

        logger.debug do
          http.set_debug_output(logger)
          "HTTP path: #{endpoint}"
        end

        http.request(request)
      end

      def build_http
        uri = URI.parse(configuration[:url])
        http = Net::HTTP.new(uri.host, uri.port)
        http.open_timeout = configuration[:open_timeout] || OPEN_TIMEOUT_DEFAULT # seconds
        http.read_timeout = configuration[:read_timeout] || READ_TIMEOUT_DEFAULT # seconds
        http.use_ssl = (ssl_verify != false)
        http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless ssl_verify

        logger.debug { "HTTP connection: #{http.inspect}" }

        http
      end

      def get_raw_response_with_retries(location)
        response = Net::HTTP.get_response(URI(location))
        logger.debug { "Response body: #{response.body}" }
        process_response_with_retries(response)
      end

      def endpoint_with_params(endpoint, query_params)
        "#{endpoint}?#{URI::DEFAULT_PARSER.escape(query_params)}"
      end
    end
  end
end