summaryrefslogtreecommitdiffstats
path: root/lib/puppet/network/http/handler.rb
blob: 03d24b3feb6500bf220db6b4e332be81b786918e (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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
module Puppet::Network::HTTP
end

require 'puppet/network/http/api/v1'
require 'puppet/network/rest_authorization'
require 'puppet/network/rights'
require 'resolv'

module Puppet::Network::HTTP::Handler
  include Puppet::Network::HTTP::API::V1
  include Puppet::Network::RestAuthorization

  attr_reader :server, :handler

  # Retrieve the accept header from the http request.
  def accept_header(request)
    raise NotImplementedError
  end

  # Retrieve the Content-Type header from the http request.
  def content_type_header(request)
    raise NotImplementedError
  end

  # Which format to use when serializing our response or interpreting the request.
  # IF the client provided a Content-Type use this, otherwise use the Accept header
  # and just pick the first value.
  def format_to_use(request)
    unless header = accept_header(request)
      raise ArgumentError, "An Accept header must be provided to pick the right format"
    end

    format = nil
    header.split(/,\s*/).each do |name|
      next unless format = Puppet::Network::FormatHandler.format(name)
      next unless format.suitable?
      return format
    end

    raise "No specified acceptable formats (#{header}) are functional on this machine"
  end

  def request_format(request)
    if header = content_type_header(request)
      header.gsub!(/\s*;.*$/,'') # strip any charset
      format = Puppet::Network::FormatHandler.mime(header)
      raise "Client sent a mime-type (#{header}) that doesn't correspond to a format we support" if format.nil?
      return format.name.to_s if format.suitable?
    end

    raise "No Content-Type header was received, it isn't possible to unserialize the request"
  end

  def format_to_mime(format)
    format.is_a?(Puppet::Network::Format) ? format.mime : format
  end

  def initialize_for_puppet(server)
    @server = server
  end

  # handle an HTTP request
  def process(request, response)
    indirection_request = uri2indirection(http_method(request), path(request), params(request))

    check_authorization(indirection_request)

    send("do_#{indirection_request.method}", indirection_request, request, response)
  rescue SystemExit,NoMemoryError
    raise
  rescue Exception => e
    return do_exception(response, e)
  end

  # Set the response up, with the body and status.
  def set_response(response, body, status = 200)
    raise NotImplementedError
  end

  # Set the specified format as the content type of the response.
  def set_content_type(response, format)
    raise NotImplementedError
  end

  def do_exception(response, exception, status=400)
    if exception.is_a?(Puppet::Network::AuthorizationError)
      # make sure we return the correct status code
      # for authorization issues
      status = 403 if status == 400
    end
    if exception.is_a?(Exception)
      puts exception.backtrace if Puppet[:trace]
      Puppet.err(exception)
    end
    set_content_type(response, "text/plain")
    set_response(response, exception.to_s, status)
  end

  # Execute our find.
  def do_find(indirection_request, request, response)
    unless result = indirection_request.model.find(indirection_request.key, indirection_request.to_hash)
      Puppet.info("Could not find #{indirection_request.indirection_name} for '#{indirection_request.key}'")
      return do_exception(response, "Could not find #{indirection_request.indirection_name} #{indirection_request.key}", 404)
    end

    # The encoding of the result must include the format to use,
    # and it needs to be used for both the rendering and as
    # the content type.
    format = format_to_use(request)
    set_content_type(response, format)

    set_response(response, result.render(format))
  end

  # Execute our search.
  def do_search(indirection_request, request, response)
    result = indirection_request.model.search(indirection_request.key, indirection_request.to_hash)

    if result.nil? or (result.is_a?(Array) and result.empty?)
      return do_exception(response, "Could not find instances in #{indirection_request.indirection_name} with '#{indirection_request.to_hash.inspect}'", 404)
    end

    format = format_to_use(request)
    set_content_type(response, format)

    set_response(response, indirection_request.model.render_multiple(format, result))
  end

  # Execute our destroy.
  def do_destroy(indirection_request, request, response)
    result = indirection_request.model.destroy(indirection_request.key, indirection_request.to_hash)

    return_yaml_response(response, result)
  end

  # Execute our save.
  def do_save(indirection_request, request, response)
    data = body(request).to_s
    raise ArgumentError, "No data to save" if !data or data.empty?

    format = request_format(request)
    obj = indirection_request.model.convert_from(format, data)
    result = save_object(indirection_request, obj)
    return_yaml_response(response, result)
  end

  # resolve node name from peer's ip address
  # this is used when the request is unauthenticated
  def resolve_node(result)
    begin
      return Resolv.getname(result[:ip])
    rescue => detail
      Puppet.err "Could not resolve #{result[:ip]}: #{detail}"
    end
    result[:ip]
  end

  private

  def return_yaml_response(response, body)
    set_content_type(response, Puppet::Network::FormatHandler.format("yaml"))
    set_response(response, body.to_yaml)
  end

  # LAK:NOTE This has to be here for testing; it's a stub-point so
  # we keep infinite recursion from happening.
  def save_object(ind_request, object)
    object.save(ind_request.key)
  end

  def get?(request)
    http_method(request) == 'GET'
  end

  def put?(request)
    http_method(request) == 'PUT'
  end

  def delete?(request)
    http_method(request) == 'DELETE'
  end

  # methods to be overridden by the including web server class

  def http_method(request)
    raise NotImplementedError
  end

  def path(request)
    raise NotImplementedError
  end

  def request_key(request)
    raise NotImplementedError
  end

  def body(request)
    raise NotImplementedError
  end

  def params(request)
    raise NotImplementedError
  end

  def decode_params(params)
    params.inject({}) do |result, ary|
      param, value = ary
      next result if param.nil? || param.empty?

      param = param.to_sym

      # These shouldn't be allowed to be set by clients
      # in the query string, for security reasons.
      next result if param == :node
      next result if param == :ip
      value = CGI.unescape(value)
      if value =~ /^---/
        value = YAML.load(value)
      else
        value = true if value == "true"
        value = false if value == "false"
        value = Integer(value) if value =~ /^\d+$/
        value = value.to_f if value =~ /^\d+\.\d+$/
      end
      result[param] = value
      result
    end
  end
end