class Puppetserver::Ca::Utils::HttpClient

Utilities for doing HTTPS against the CA that wraps Net::HTTP constructs

Constants

Result

Just provide the bits of Net::HTTPResponse we care about

URL

Like URI, but not… maybe of suspicious value

Attributes

store[R]

Public Class Methods

new(logger, settings, with_client_cert: true) click to toggle source

Not all connections require a client cert to be present. For example, when querying the status endpoint.

# File lib/puppetserver/ca/utils/http_client.rb, line 17
def initialize(logger, settings, with_client_cert: true)
  @default_headers = make_headers(ENV['HOME'])
  @logger = logger
  @store = make_store(settings[:localcacert],
                      settings[:certificate_revocation],
                      settings[:hostcrl])

  if with_client_cert
    @cert = load_cert(settings[:hostcert])
    @key = load_key(settings[:hostprivkey])
  else
    @cert = nil
    @key = nil
  end
end

Private Class Methods

check_server_online(settings, logger) click to toggle source

Queries the simple status endpoint for the status of the CA service. Returns true if it receives back a response of “running”, and false if no connection can be made, or a different response is received.

# File lib/puppetserver/ca/utils/http_client.rb, line 203
def self.check_server_online(settings, logger)
  status_url = URL.new('https', settings[:ca_server], settings[:ca_port], 'status', 'v1', 'simple', 'ca')
  begin
    # Generating certs offline is necessary if the server cert has been destroyed
    # or compromised. Since querying the status endpoint does not require a client cert, and
    # we commonly won't have one, don't require one for creating the connection.
    # Additionally, we want to ensure the server is stopped before migrating the CA dir to
    # avoid issues with writing to the CA dir and moving it.
    self.new(logger, settings, with_client_cert: false).with_connection(status_url) do |conn|
      result = conn.get
      if result.body == "running"
        logger.err "Puppetserver service is running. Please stop it before attempting to run this command."
        true
      else
        false
      end
    end
  rescue Puppetserver::Ca::ConnectionFailed => e
    if e.wrapped.is_a? Errno::ECONNREFUSED
      return false
    else
      raise e
    end
  end
end

Public Instance Methods

load_cert(path) click to toggle source
# File lib/puppetserver/ca/utils/http_client.rb, line 33
def load_cert(path)
  load_with_errors(path, 'hostcert') do |content|
    OpenSSL::X509::Certificate.new(content)
  end
end
load_key(path) click to toggle source
# File lib/puppetserver/ca/utils/http_client.rb, line 39
def load_key(path)
  load_with_errors(path, 'hostprivkey') do |content|
    OpenSSL::PKey.read(content)
  end
end
with_connection(url, &block) click to toggle source

Takes an instance URL (defined lower in the file), and creates a connection. The given block is passed our own Connection object. The Connection object should have HTTP verbs defined on it that take a body (and optional overrides). Returns whatever the block given returned.

# File lib/puppetserver/ca/utils/http_client.rb, line 49
def with_connection(url, &block)
  request = ->(conn) { block.call(Connection.new(conn, url, @logger, @default_headers)) }

  begin
    Net::HTTP.start(url.host, url.port,
                    use_ssl: true, cert_store: @store,
                    cert: @cert, key: @key,
                    &request)
  rescue StandardError => e
    raise ConnectionFailed.create(e,
            "Failed connecting to #{url.full_url}\n" +
            "  Root cause: #{e.message}")
  end
end

Private Instance Methods

load_with_errors(path, setting, &block) click to toggle source
# File lib/puppetserver/ca/utils/http_client.rb, line 81
def load_with_errors(path, setting, &block)
  begin
    content = File.read(path)
    block.call(content)
  rescue Errno::ENOENT => e
    raise FileNotFound.create(e,
            "Could not find '#{setting}' at '#{path}'")

  rescue OpenSSL::OpenSSLError => e
    raise InvalidX509Object.create(e,
            "Could not parse '#{setting}' at '#{path}'.\n" +
            "  OpenSSL returned: #{e.message}")
  end
end
make_headers(home) click to toggle source
# File lib/puppetserver/ca/utils/http_client.rb, line 66
def make_headers(home)
  headers = {
    'User-Agent'   => 'PuppetserverCaCli',
    'Content-Type' => 'application/json',
    'Accept'       => 'application/json'
  }

  token_path = "#{home}/.puppetlabs/token"
  if File.exist?(token_path)
    headers['X-Authentication'] = File.read(token_path)
  end

  headers
end
make_store(bundle, crl_usage, crls = nil) click to toggle source
# File lib/puppetserver/ca/utils/http_client.rb, line 178
def make_store(bundle, crl_usage, crls = nil)
  store = OpenSSL::X509::Store.new
  store.purpose = OpenSSL::X509::PURPOSE_ANY
  store.add_file(bundle)

  if crl_usage != :ignore

    flags = OpenSSL::X509::V_FLAG_CRL_CHECK
    if crl_usage == :chain
      flags |= OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
    end

    store.flags = flags
    delimiter = /-----BEGIN X509 CRL-----.*?-----END X509 CRL-----/m
    File.read(crls).scan(delimiter).each do |crl|
      store.add_crl(OpenSSL::X509::CRL.new(crl))
    end
  end

  store
end