summaryrefslogtreecommitdiffstats
path: root/lib/puppet/indirector/rest.rb
blob: 19daff51dfd848bbf5da1421aac03a9da27d9188 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
require 'net/http'
require 'uri'

require 'puppet/network/http_pool'
require 'puppet/network/http/api/v1'
require 'puppet/network/http/compression'

# Access objects via REST
class Puppet::Indirector::REST < Puppet::Indirector::Terminus
  include Puppet::Network::HTTP::API::V1
  include Puppet::Network::HTTP::Compression.module

  class << self
    attr_reader :server_setting, :port_setting
  end

  # Specify the setting that we should use to get the server name.
  def self.use_server_setting(setting)
    @server_setting = setting
  end

  def self.server
    Puppet.settings[server_setting || :server]
  end

  # Specify the setting that we should use to get the port.
  def self.use_port_setting(setting)
    @port_setting = setting
  end

  def self.port
    Puppet.settings[port_setting || :masterport].to_i
  end

  # Figure out the content type, turn that into a format, and use the format
  # to extract the body of the response.
  def deserialize(response, multiple = false)
    case response.code
    when "404"
      return nil
    when /^2/
      raise "No content type in http response; cannot parse" unless response['content-type']

      content_type = response['content-type'].gsub(/\s*;.*$/,'') # strip any appended charset

      body = uncompress_body(response)

      # Convert the response to a deserialized object.
      if multiple
        model.convert_from_multiple(content_type, body)
      else
        model.convert_from(content_type, body)
      end
    else
      # Raise the http error if we didn't get a 'success' of some kind.
      raise convert_to_http_error(response)
    end
  end

  def convert_to_http_error(response)
    message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}"
    Net::HTTPError.new(message, response)
  end

  # Provide appropriate headers.
  def headers
    add_accept_encoding({"Accept" => model.supported_formats.join(", ")})
  end

  def network(request)
    Puppet::Network::HttpPool.http_instance(request.server || self.class.server, request.port || self.class.port)
  end

  [:get, :post, :head, :delete, :put].each do |method|
    define_method "http_#{method}" do |request, *args|
      http_request(method, request, *args)
    end
  end

  def http_request(method, request, *args)
    http_connection = network(request)
    peer_certs = []

    # We add the callback to collect the certificates for use in constructing
    # the error message if the verification failed.  This is necessary since we
    # don't have direct access to the cert that we expected the connection to
    # use otherwise.
    #
    http_connection.verify_callback = proc do |preverify_ok, ssl_context|
      peer_certs << Puppet::SSL::Certificate.from_s(ssl_context.current_cert.to_pem)
      preverify_ok
    end

    http_connection.send(method, *args)
  rescue OpenSSL::SSL::SSLError => error
    if error.message.include? "certificate verify failed"
      raise Puppet::Error, "#{error.message}.  This is often because the time is out of sync on the server or client"
    elsif error.message.include? "hostname was not match"
      raise unless cert = peer_certs.find { |c| c.name !~ /^puppet ca/i }

      valid_certnames = [cert.name, *cert.alternate_names].uniq
      msg = valid_certnames.length > 1 ? "one of #{valid_certnames.join(', ')}" : valid_certnames.first

      raise Puppet::Error, "Server hostname '#{http_connection.address}' did not match server certificate; expected #{msg}"
    else
      raise
    end
  end

  def find(request)
    uri, body = request_to_uri_and_body(request)
    uri_with_query_string = "#{uri}?#{body}"
    # WEBrick in Ruby 1.9.1 only supports up to 1024 character lines in an HTTP request
    # http://redmine.ruby-lang.org/issues/show/3991
    response = if "GET #{uri_with_query_string} HTTP/1.1\r\n".length > 1024
      http_post(request, uri, body, headers)
    else
      http_get(request, uri_with_query_string, headers)
    end
    result = deserialize response
    result.name = request.key if result.respond_to?(:name=)
    result
  end

  def head(request)
    response = http_head(request, indirection2uri(request), headers)
    case response.code
    when "404"
      return false
    when /^2/
      return true
    else
      # Raise the http error if we didn't get a 'success' of some kind.
      raise convert_to_http_error(response)
    end
  end

  def search(request)
    unless result = deserialize(http_get(request, indirection2uri(request), headers), true)
      return []
    end
    result
  end

  def destroy(request)
    raise ArgumentError, "DELETE does not accept options" unless request.options.empty?
    deserialize http_delete(request, indirection2uri(request), headers)
  end

  def save(request)
    raise ArgumentError, "PUT does not accept options" unless request.options.empty?
    deserialize http_put(request, indirection2uri(request), request.instance.render, headers.merge({ "Content-Type" => request.instance.mime }))
  end

  private

  def environment
    Puppet::Node::Environment.new
  end
end