SUSE/connect

View on GitHub
lib/suse/connect/connection.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'openssl'
require 'net/http'
require 'ostruct'
require 'json'
require 'suse/toolkit/utilities'

module SUSE
  module Connect
    # Establishing a connection to SCC REST API and calling
    class Connection
      include Logger

      VERB_TO_CLASS = {
        get: Net::HTTP::Get,
        post: Net::HTTP::Post,
        put: Net::HTTP::Put,
        delete: Net::HTTP::Delete
      }

      attr_accessor :debug, :http, :auth, :language

      def initialize(endpoint, language: nil, insecure: false, debug: false, verify_callback: nil)
        endpoint         = prefix_protocol(endpoint)
        uri              = URI.parse(endpoint)
        http             = Net::HTTP.new(uri.host, uri.port)

        if http.proxy? && proxy_system_enabled?
          proxy_address = http.proxy_uri.hostname
          proxy_port = http.proxy_uri.port
          proxy_user = SUSE::Toolkit::CurlrcDotfile.new.username
          proxy_pass = SUSE::Toolkit::CurlrcDotfile.new.password
          log.debug("Using proxy: #{http.proxy_uri}")
          http = Net::HTTP.new(uri.host, uri.port, proxy_address, proxy_port, proxy_user, proxy_pass)
        else
          # With the third parameter set to nil, we completely disable http
          # proxy detection so to avoid problems on this down the road.
          http = Net::HTTP.new(uri.host, uri.port, nil)
        end

        http.use_ssl     = uri.is_a? URI::HTTPS
        http.verify_mode = insecure ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
        http.read_timeout = 60

        @http            = http
        @language        = language
        @debug           = debug
        @http.set_debug_output(STDERR) if debug
        self.verify_callback = verify_callback
      end

      VERB_TO_CLASS.each_key do |name_for_method|
        define_method name_for_method do |path, auth: nil, params: {}|
          @auth = auth
          response = json_request(name_for_method.downcase.to_sym, path, params)

          unless response.success
            error = ApiError.new(response)
            raise(error, error.message)
          end

          response
        end
      end

      private

      # Returns true if a specific configuration on the system allows or not to
      # use a configured proxy.
      #
      # This is usually already handled by the NET::HTTP library, but it only
      # checks for some very specific environment variables (see
      # `URI::HTTP#find_proxy`). In some environments (e.g. SUSE systems using
      # the `/etc/sysconfig/proxy` configuration file) there might be the
      # environment variable `PROXY_ENABLED`, which allows sysadmins to switch
      # on/off the proxy setup.
      def proxy_system_enabled?
        value = ENV['proxy_enabled'] || ENV['PROXY_ENABLED']

        # If this environment variable is not set, then we will return true so
        # to not interfere with the detection from `net/http`. That is, since
        # this is not enforced at the system level, then net/http should decide
        # as usual.
        return true if value.nil? || value == ''

        value = value.strip.downcase
        value == 'y' || value == 'yes' || value == 't' || value == 'true'
      end

      def path_with_params(path, params)
        return path if params.nil? || params.empty?
        encoded_params = URI.encode_www_form(params)
        [path, encoded_params].join('?')
      end

      def prefix_protocol(endpoint)
        if endpoint[%r{^(http|https):\/\/}]
          endpoint
        else
          "https://#{endpoint}"
        end
      end

      def json_request(method, path, params = {})
        # for :get requests, the params need to go to the url, for other requests into the body
        if method == :get
          request = VERB_TO_CLASS[method].new(path_with_params(path, params))
        else
          request = VERB_TO_CLASS[method].new(path)
          request.body = params.to_json unless params.nil? || params.empty?
        end
        add_headers(request)

        response      = @http.request(request)
        response_body = JSON.parse(response.body) unless response.body.to_s.empty?

        update_system_token!(response)

        OpenStruct.new(
          code: response.code.to_i,
          headers: response.to_hash,
          body: response_body,
          http_message: response.message,
          success: response.is_a?(Net::HTTPSuccess)
        )
      rescue Zlib::Error
        raise SUSE::Connect::NetworkError, 'Check your network connection and try again. If it keeps failing, report a bug.'
      end

      # Given an HTTP response, try to update the credentials file with the
      # given 'System-Token' header.
      def update_system_token!(response)
        return unless System.credentials?

        value = response.to_hash[SUSE::Toolkit::Utilities::SYSTEM_TOKEN_HEADER.downcase]
        token = value.first.strip unless value.nil? || value.first.nil?
        return if token.nil? || token.empty?

        creds = System.credentials
        creds.system_token = token
        creds.write
      end

      def add_headers(request)
        # The authorization might be a hash, which means that both an encoded
        # authorization and a system token are given.
        if auth.is_a?(Hash)
          request['Authorization'] = auth[:encoded]
          # Note that `Net/HTTP` ignore headers with an empty value (i.e. nil or
          # ''). Thus, if there is no system token yet for this system, assign a
          # string with at least one space character, so it's not ignored but
          # perceived as empty by the server.
          request[SUSE::Toolkit::Utilities::SYSTEM_TOKEN_HEADER] = auth[:token] || ' '
        else
          request['Authorization'] = auth
        end

        request['Content-Type']    = 'application/json'
        request['Accept']          = "application/json,application/vnd.scc.suse.com.#{SUSE::Connect::Api::VERSION}+json"
        request['Accept-Language'] = language
        # no gzip compression for easier debugging
        request['Accept-Encoding'] = 'identity' if debug
        request['User-Agent']      = "SUSEConnect/#{SUSE::Connect::VERSION}"
      end

      # set a verify_callback to HTTP object, use a custom callback
      # or the default if not set
      def verify_callback=(callback)
        if callback
          log.debug "Using custom verify_callback: #{callback.source_location.map(&:to_s).join(':')}"
          http.verify_callback = callback
        else
          # log some error details which are not included in the SSL exception
          http.verify_callback = lambda do |verify_ok, context|
            unless verify_ok
              log.error "SSL verification failed: #{context.error_string}"
              log.error "Certificate issuer: #{context.current_cert.issuer}"
              log.error "Certificate subject: #{context.current_cert.subject}"
            end
            verify_ok
          end
        end
      end
    end
  end
end