Class: Sensu::API::HTTPHandler

Inherits:
EM::HttpServer::Server
  • Object
show all
Includes:
Routes, Utilities::FilterResponseContent
Defined in:
lib/sensu/api/http_handler.rb

Constant Summary

Constants included from Routes

Routes::DELETE_METHOD, Routes::GET_METHOD, Routes::GET_ROUTES, Routes::HEAD_METHOD, Routes::OPTIONS_METHOD, Routes::POST_METHOD, Routes::ROUTES

Constants included from Routes::Silenced

Routes::Silenced::SILENCED_CHECK_URI, Routes::Silenced::SILENCED_CLEAR_URI, Routes::Silenced::SILENCED_ID_URI, Routes::Silenced::SILENCED_SUBSCRIPTION_URI, Routes::Silenced::SILENCED_URI

Constants included from Routes::Results

Routes::Results::RESULTS_CLIENT_URI, Routes::Results::RESULTS_URI, Routes::Results::RESULT_URI

Constants included from Routes::Stashes

Routes::Stashes::STASHES_URI, Routes::Stashes::STASH_URI

Constants included from Routes::Aggregates

Routes::Aggregates::AGGREGATES_URI, Routes::Aggregates::AGGREGATE_CHECKS_URI, Routes::Aggregates::AGGREGATE_CLIENTS_URI, Routes::Aggregates::AGGREGATE_RESULTS_SEVERITY_URI, Routes::Aggregates::AGGREGATE_URI

Constants included from Routes::Resolve

Routes::Resolve::RESOLVE_URI

Constants included from Routes::Events

Routes::Events::EVENTS_CLIENT_URI, Routes::Events::EVENTS_URI, Routes::Events::EVENT_URI

Constants included from Routes::Request

Routes::Request::REQUEST_URI

Constants included from Routes::Checks

Routes::Checks::CHECKS_URI, Routes::Checks::CHECK_URI

Constants included from Routes::Clients

Routes::Clients::CLIENTS_URI, Routes::Clients::CLIENT_HISTORY_URI, Routes::Clients::CLIENT_URI

Constants included from Routes::Health

Routes::Health::HEALTH_URI

Constants included from Routes::Info

Routes::Info::INFO_URI

Constants included from Routes::Settings

Routes::Settings::SETTINGS_URI

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Routes::Silenced

#fetch_silenced, #get_silenced, #get_silenced_check, #get_silenced_id, #get_silenced_subscription, #post_silenced, #post_silenced_clear

Methods included from Routes::Results

#delete_result, #get_result, #get_results, #get_results_client, #post_results

Methods included from Routes::Stashes

#delete_stash, #get_stash, #get_stashes, #post_stash, #post_stashes

Methods included from Routes::Aggregates

#delete_aggregate, #get_aggregate, #get_aggregate_checks, #get_aggregate_clients, #get_aggregate_results_severity, #get_aggregates

Methods included from Routes::Resolve

#post_resolve

Methods included from Routes::Events

#delete_event, #get_event, #get_events, #get_events_client

Methods included from Routes::Request

#post_request

Methods included from Routes::Checks

#get_check, #get_checks

Methods included from Routes::Clients

#delete_client, #get_client, #get_client_history, #get_clients, #post_clients

Methods included from Routes::Health

#get_health

Methods included from Routes::Info

#get_info

Methods included from Routes::Settings

#get_settings

Instance Attribute Details

#loggerObject

Returns the value of attribute logger.



16
17
18
# File 'lib/sensu/api/http_handler.rb', line 16

def logger
  @logger
end

#redisObject

Returns the value of attribute redis.



16
17
18
# File 'lib/sensu/api/http_handler.rb', line 16

def redis
  @redis
end

#settingsObject

Returns the value of attribute settings.



16
17
18
# File 'lib/sensu/api/http_handler.rb', line 16

def settings
  @settings
end

#transportObject

Returns the value of attribute transport.



16
17
18
# File 'lib/sensu/api/http_handler.rb', line 16

def transport
  @transport
end

Instance Method Details

#accepted!Object

Respond to the HTTP request with a ‘202` (Accepted) response.



273
274
275
276
277
# File 'lib/sensu/api/http_handler.rb', line 273

def accepted!
  @response_status = 202
  @response_status_string = "Accepted"
  respond
end

#allowed_http_methods?Array

Determine the allowed HTTP methods for a route. The route regular expressions and associated route method calls are provided by ‘ROUTES`. This method returns an array of HTTP methods that have a route that matches the HTTP request URI.

Returns:

  • (Array)


342
343
344
345
346
347
348
349
# File 'lib/sensu/api/http_handler.rb', line 342

def allowed_http_methods?
  ROUTES.map { |http_method, routes|
    match = routes.detect do |route|
      @http_request_uri =~ route[0]
    end
    match ? http_method : nil
  }.flatten.compact
end

#authorized?TrueClass, FalseClass

Determine if an HTTP request is authorized. This method compares the configured API user and password (if any) with the HTTP request basic authentication credentials. No authentication is done if the API user and password are not configured. OPTIONS HTTP requests bypass authentication.

Returns:

  • (TrueClass, FalseClass)


219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/sensu/api/http_handler.rb', line 219

def authorized?
  api = @settings[:api]
  if api && api[:user] && api[:password]
    if @http_request_method == OPTIONS_METHOD
      true
    elsif @http[:authorization]
      scheme, base64 = @http[:authorization].split("\s")
      if scheme == "Basic"
        user, password = Base64.decode64(base64).split(":")
        user == api[:user] && password == api[:password]
      else
        false
      end
    else
      false
    end
  else
    true
  end
end

#bad_request!Object

Respond to the HTTP request with a ‘400` (Bad Request) response.



289
290
291
292
293
# File 'lib/sensu/api/http_handler.rb', line 289

def bad_request!
  @response_status = 400
  @response_status_string = "Bad Request"
  respond
end

#connected?Boolean

Determine if the API is connected to Redis and the Transport. This method sets the ‘@response_content` if the API is not connected or it has not yet initialized the connection objects. The `/info` and `/health` routes are excluded from the connectivity checks.

Returns:

  • (Boolean)


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/sensu/api/http_handler.rb', line 245

def connected?
  connected = true
  if @redis && @transport
    unless @http_request_uri =~ INFO_URI || @http_request_uri =~ HEALTH_URI
      unless @redis.connected?
        @response_content = {:error => "not connected to redis"}
        connected = false
      end
      unless @transport.connected?
        @response_content = {:error => "not connected to transport"}
        connected = false
      end
    end
  else
    @response_content = {:error => "redis and transport connections not initialized"}
    connected = false
  end
  connected
end

#create_responseObject

Create an EM HTTP Server HTTP response object, ‘@response`. The response object is use to build up the response status, status string, content type, and content. The response object is responsible for sending the HTTP response to the HTTP client and closing the connection afterwards.

Returns:

  • (Object)


136
137
138
# File 'lib/sensu/api/http_handler.rb', line 136

def create_response
  @response = EM::DelegatedHttpResponse.new(self)
end

#created!Object

Respond to the HTTP request with a ‘201` (Created) response.



266
267
268
269
270
# File 'lib/sensu/api/http_handler.rb', line 266

def created!
  @response_status = 201
  @response_status_string = "Created"
  respond
end

#determine_route_methodSymbol

Determine the route method for the HTTP request method and URI. The route regular expressions and associated route method calls are provided by ‘ROUTES`. This method will return the first route method name (Ruby symbol) that has matching URI regular expression. If an HTTP method is not supported, or there is not a matching regular expression, `nil` will be returned.

Returns:

  • (Symbol)


360
361
362
363
364
365
366
367
368
369
# File 'lib/sensu/api/http_handler.rb', line 360

def determine_route_method
  if ROUTES.has_key?(@http_request_method)
    route = ROUTES[@http_request_method].detect do |route|
      @http_request_uri =~ route[0]
    end
    route ? route[1] : nil
  else
    nil
  end
end

#error!Object

Respond to the HTTP request with a ‘500` (Internal Server Error) response.



330
331
332
333
334
# File 'lib/sensu/api/http_handler.rb', line 330

def error!
  @response_status = 500
  @response_status_string = "Internal Server Error"
  respond
end

#http_request_errback(error) ⇒ Object

Catch uncaught/unexpected errors, log them, and attempt to respond with a ‘500` (Internal Server Error) HTTP response. This method is called by EM HTTP Server.

Parameters:

  • error (Object)


424
425
426
427
428
429
430
# File 'lib/sensu/api/http_handler.rb', line 424

def http_request_errback(error)
  @logger.error("unexpected api error", {
    :error => error.to_s,
    :backtrace => error.backtrace.join("\n")
  })
  error! rescue nil
end

#integer_parameter(value) ⇒ Integer?

Determine if a parameter has an integer value and if so return it as one. This method will return ‘nil` if the parameter value is not an integer.

Parameters:

  • value (String)

Returns:

  • (Integer, nil)


98
99
100
# File 'lib/sensu/api/http_handler.rb', line 98

def integer_parameter(value)
  value =~ /\A[0-9]+\z/ ? value.to_i : nil
end

#log_requestObject

Log the HTTP request. The debug log level is used for requests as response logging includes the same information.



45
46
47
# File 'lib/sensu/api/http_handler.rb', line 45

def log_request
  @logger.debug("api request", request_details)
end

#log_responseObject

Log the HTTP response. This method calculates the request/response time. The debug log level is used for the response body log event, as it is generally very verbose and unnecessary in most cases.



53
54
55
56
57
58
59
60
# File 'lib/sensu/api/http_handler.rb', line 53

def log_response
  @logger.info("api response", {
    :request => request_details,
    :status => @response.status,
    :content_length => @response.content.to_s.bytesize,
    :time => (Time.now.to_f - @request_start_time).round(3)
  })
end

#method_not_allowed!(allowed_http_methods = []) ⇒ Object

Respond to the HTTP request with a ‘405` (Method Not Allowed) response.



313
314
315
316
317
318
# File 'lib/sensu/api/http_handler.rb', line 313

def method_not_allowed!(allowed_http_methods=[])
  @response.headers["Allow"] = allowed_http_methods.join(", ")
  @response_status = 405
  @response_status_string = "Method Not Allowed"
  respond
end

#no_content!Object

Respond to the HTTP request with a ‘204` (No Content) response.



281
282
283
284
285
# File 'lib/sensu/api/http_handler.rb', line 281

def no_content!
  @response_status = 204
  @response_status_string = "No Content"
  respond
end

#not_found!Object

Respond to the HTTP request with a ‘404` (Not Found) response.



306
307
308
309
310
# File 'lib/sensu/api/http_handler.rb', line 306

def not_found!
  @response_status = 404
  @response_status_string = "Not Found"
  respond
end

#pagination(items) ⇒ Array

Paginate the provided items. This method uses two HTTP query parameters to determine how to paginate the items, ‘limit` and `offset`. The parameter `limit` specifies how many items are to be returned in the response. The parameter `offset` specifies the items array index, skipping a number of items. This method sets the “X-Pagination” HTTP response header to a JSON object containing the `limit`, `offset` and `total` number of items that are being paginated.

Parameters:

  • items (Array)

Returns:

  • (Array)

    paginated items.



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/sensu/api/http_handler.rb', line 174

def pagination(items)
  limit = integer_parameter(@params[:limit])
  offset = integer_parameter(@params[:offset]) || 0
  unless limit.nil?
    @response.headers["X-Pagination"] = Sensu::JSON.dump(
      :limit => limit,
      :offset => offset,
      :total => items.length
    )
    paginated = items.slice(offset, limit)
    Array(paginated)
  else
    items
  end
end

#parse_parametersObject

Parse the HTTP request query string for parameters. This method creates ‘@params`, a hash of parsed query parameters, used by the API routes. This method also creates `@filter_params`, a hash of parsed response content filter parameters.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/sensu/api/http_handler.rb', line 77

def parse_parameters
  @params = {}
  if @http_query_string
    @http_query_string.split("&").each do |pair|
      key, value = pair.split("=")
      @params[key.to_sym] = value
      if key.start_with?("filter.")
        filter_param = key.sub(/^filter\./, "")
        @filter_params ||= {}
        @filter_params[filter_param] = value
      end
    end
  end
end

#parse_uri(regex) ⇒ Array

Parse the HTTP request URI using a regular expression, returning the URI unescaped match data values.

Parameters:

  • regex (Regexp)

Returns:

  • (Array)

    URI unescaped match data values.



67
68
69
70
# File 'lib/sensu/api/http_handler.rb', line 67

def parse_uri(regex)
  uri_match = regex.match(@http_request_uri)[1..-1]
  uri_match.map { |s| URI.unescape(s) }
end

#precondition_failed!Object

Respond to the HTTP request with a ‘412` (Precondition Failed) response.



322
323
324
325
326
# File 'lib/sensu/api/http_handler.rb', line 322

def precondition_failed!
  @response_status = 412
  @response_status_string = "Precondition Failed"
  respond
end

#process_http_requestObject

Process a HTTP request. Log the request, parse the HTTP query parameters, create the HTTP response object, set the cors HTTP response headers, determine if the request is authorized, determine if the API is connected to Redis and the Transport, and then route the HTTP request (responding to the request). This method is called by EM HTTP Server when handling a new connection.



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# File 'lib/sensu/api/http_handler.rb', line 403

def process_http_request
  log_request
  parse_parameters
  create_response
  set_headers
  if authorized?
    if connected?
      route_request
    else
      error!
    end
  else
    unauthorized!
  end
end

#read_data(rules = {}) {|Object| ... } ⇒ Object

Read JSON data from the HTTP request content and validate it with the provided rules. If the HTTP request content does not contain valid JSON or it does not pass validation, this method returns a ‘400` (Bad Request) HTTP response.

Parameters:

  • rules (Hash) (defaults to: {})

    containing the validation rules.

Yields:

  • (Object)

    the callback/block called with the data after successfully parsing and validating the the HTTP request content.



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/sensu/api/http_handler.rb', line 110

def read_data(rules={})
  begin
    data = Sensu::JSON.load(@http_content)
    valid = data.is_a?(Hash) && rules.all? do |key, rule|
      value = data[key]
      (Array(rule[:type]).any? {|type| value.is_a?(type)} ||
        (rule[:nil_ok] && value.nil?)) && (value.nil? || rule[:regex].nil?) ||
        (rule[:regex] && (value =~ rule[:regex]) == 0)
    end
    if valid
      yield(data)
    else
      bad_request!
    end
  rescue Sensu::JSON::ParseError
    bad_request!
  end
end

#request_detailsObject

Create a hash containing the HTTP request details. This method determines the remote address for the HTTP client (using EventMachine Connection ‘get_peername()`).



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/sensu/api/http_handler.rb', line 23

def request_details
  return @request_details if @request_details
  @request_id = @http.fetch(:x_request_id, random_uuid)
  @request_start_time = Time.now.to_f
  _, remote_address = Socket.unpack_sockaddr_in(get_peername)
  @request_details = {
    :request_id => @request_id,
    :remote_address => remote_address,
    :user_agent => @http[:user_agent],
    :method => @http_request_method,
    :uri => @http_request_uri,
    :query_string => @http_query_string,
    :body => @http_content
  }
  if @http[:x_forwarded_for]
    @request_details[:x_forwarded_for] = @http[:x_forwarded_for]
  end
  @request_details
end

#respondObject

Respond to an HTTP request. The routes set ‘@response_status`, `@response_status_string`, and `@response_content` appropriately. The HTTP response status defaults to `200` with the status string `OK`. If filter params were provided, `@response_content` is filtered (mutated). The Sensu API only returns JSON response content, `@response_content` is assumed to be a Ruby object that can be serialized as JSON.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/sensu/api/http_handler.rb', line 197

def respond
  @response.status = @response_status || 200
  @response.status_string = @response_status_string || "OK"
  if @response_content && @http_request_method != HEAD_METHOD
    if @http_request_method == GET_METHOD && @filter_params
      filter_response_content!
    end
    @response.content_type "application/json"
    @response.content = Sensu::JSON.dump(@response_content)
  end
  @response.headers["Connection"] = "close"
  log_response
  @response.send_response
end

#route_requestObject

Route the HTTP request. OPTIONS HTTP requests will always return a ‘200` with no response content. This method uses `determine_route_method()` to determine the symbolized route method to send/call. If a route method does not exist for the HTTP request method and URI, this method uses `allowed_http_methods?()` to determine if a 404 (Not Found) or 405 (Method Not Allowed) HTTP response should be used.



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
# File 'lib/sensu/api/http_handler.rb', line 378

def route_request
  if @http_request_method == OPTIONS_METHOD
    respond
  else
    route_method = determine_route_method
    if route_method
      send(route_method)
    else
      allowed_http_methods = allowed_http_methods?
      if allowed_http_methods.empty?
        not_found!
      else
        method_not_allowed!(allowed_http_methods)
      end
    end
  end
end

#set_cors_headersObject

Set the cors (Cross-origin resource sharing) HTTP headers.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/sensu/api/http_handler.rb', line 141

def set_cors_headers
  api = @settings[:api] || {}
  api[:cors] ||= {
    "Origin" => "*",
    "Methods" => "GET, POST, PUT, DELETE, OPTIONS",
    "Credentials" => "true",
    "Headers" => "Origin, X-Requested-With, Content-Type, Accept, Authorization"
  }
  if api[:cors].is_a?(Hash)
    api[:cors].each do |header, value|
      @response.headers["Access-Control-Allow-#{header}"] = value
    end
  end
end

#set_headersObject

Set the HTTP response headers, including the request ID and cors headers (via ‘set_cores_headers()`).



158
159
160
161
# File 'lib/sensu/api/http_handler.rb', line 158

def set_headers
  @response.headers["X-Request-ID"] = @request_id
  set_cors_headers
end

#unauthorized!Object

Respond to the HTTP request with a ‘401` (Unauthroized) response. This method sets the “WWW-Autenticate” HTTP response header.



298
299
300
301
302
303
# File 'lib/sensu/api/http_handler.rb', line 298

def unauthorized!
  @response.headers["WWW-Authenticate"] = 'Basic realm="Restricted Area"'
  @response_status = 401
  @response_status_string = "Unauthorized"
  respond
end