2017-01-05 19:29:11 +00:00
|
|
|
# encoding: utf-8
|
|
|
|
# copyright: 2017, Criteo
|
2017-02-02 21:13:03 +00:00
|
|
|
# copyright: 2017, Chef Software Inc
|
2017-01-05 19:29:11 +00:00
|
|
|
# license: Apache v2
|
|
|
|
|
2017-02-02 21:13:03 +00:00
|
|
|
require 'faraday'
|
2018-10-13 06:14:17 +00:00
|
|
|
require 'faraday_middleware'
|
2017-01-26 13:18:49 +00:00
|
|
|
require 'hashie'
|
2017-01-05 19:29:11 +00:00
|
|
|
|
|
|
|
module Inspec::Resources
|
|
|
|
class Http < Inspec.resource(1)
|
|
|
|
name 'http'
|
2018-02-19 14:26:49 +00:00
|
|
|
supports platform: 'unix'
|
2017-01-05 19:29:11 +00:00
|
|
|
desc 'Use the http InSpec audit resource to test http call.'
|
2019-03-19 14:17:32 +00:00
|
|
|
example <<~EXAMPLE
|
2017-01-05 19:29:11 +00:00
|
|
|
describe http('http://localhost:8080/ping', auth: {user: 'user', pass: 'test'}, params: {format: 'html'}) do
|
|
|
|
its('status') { should cmp 200 }
|
|
|
|
its('body') { should cmp 'pong' }
|
2017-01-26 13:18:49 +00:00
|
|
|
its('headers.Content-Type') { should cmp 'text/html' }
|
|
|
|
end
|
|
|
|
|
|
|
|
describe http('http://example.com/ping').headers do
|
|
|
|
its('Content-Length') { should cmp 258 }
|
|
|
|
its('Content-Type') { should cmp 'text/html; charset=UTF-8' }
|
2017-01-05 19:29:11 +00:00
|
|
|
end
|
2019-03-19 14:17:32 +00:00
|
|
|
EXAMPLE
|
2017-01-05 19:29:11 +00:00
|
|
|
|
2017-05-19 15:23:11 +00:00
|
|
|
def initialize(url, opts = {})
|
2017-01-05 19:29:11 +00:00
|
|
|
@url = url
|
2017-10-04 20:44:09 +00:00
|
|
|
@opts = opts
|
|
|
|
|
2018-02-13 17:42:16 +00:00
|
|
|
# Prior to InSpec 2.0 the HTTP test had to be instructed to run on the
|
|
|
|
# remote target machine. This warning will be removed after a few months
|
|
|
|
# to give users an opportunity to remove the unused option from their
|
|
|
|
# profiles.
|
|
|
|
if opts.key?(:enable_remote_worker) && !inspec.local_transport?
|
|
|
|
warn 'Ignoring `enable_remote_worker` option, the `http` resource ',
|
|
|
|
'remote worker is enabled by default for remote targets and ',
|
|
|
|
'cannot be disabled'
|
|
|
|
end
|
|
|
|
|
|
|
|
# Run locally if InSpec is ran locally and remotely if ran remotely
|
|
|
|
if inspec.local_transport?
|
2017-10-04 20:44:09 +00:00
|
|
|
@worker = Worker::Local.new(http_method, url, opts)
|
2018-02-13 17:42:16 +00:00
|
|
|
else
|
|
|
|
@worker = Worker::Remote.new(inspec, http_method, url, opts)
|
2017-10-04 20:44:09 +00:00
|
|
|
end
|
2017-01-05 19:29:11 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def status
|
2017-10-04 20:44:09 +00:00
|
|
|
@worker.status
|
|
|
|
end
|
|
|
|
|
|
|
|
def headers
|
2018-01-16 22:30:35 +00:00
|
|
|
@headers ||= Inspec::Resources::Http::Headers.create(@worker.response_headers)
|
2017-01-05 19:29:11 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def body
|
2017-10-04 20:44:09 +00:00
|
|
|
@worker.body
|
2017-01-05 19:29:11 +00:00
|
|
|
end
|
|
|
|
|
2017-10-04 20:44:09 +00:00
|
|
|
def http_method
|
|
|
|
@opts.fetch(:method, 'GET')
|
2017-01-05 19:29:11 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
2019-02-28 07:24:30 +00:00
|
|
|
if @opts and @url
|
|
|
|
"HTTP #{http_method} on #{@url}"
|
|
|
|
else
|
|
|
|
'HTTP Resource'
|
|
|
|
end
|
2017-01-05 19:29:11 +00:00
|
|
|
end
|
|
|
|
|
2017-10-04 20:44:09 +00:00
|
|
|
class Worker
|
|
|
|
class Base
|
|
|
|
attr_reader :http_method, :opts, :url
|
|
|
|
|
|
|
|
def initialize(http_method, url, opts)
|
|
|
|
@http_method = http_method
|
|
|
|
@url = url
|
|
|
|
@opts = opts
|
2018-03-28 15:22:01 +00:00
|
|
|
@response = nil
|
2017-10-04 20:44:09 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def params
|
|
|
|
opts.fetch(:params, nil)
|
|
|
|
end
|
|
|
|
|
|
|
|
def username
|
|
|
|
opts.fetch(:auth, {})[:user]
|
|
|
|
end
|
|
|
|
|
|
|
|
def password
|
|
|
|
opts.fetch(:auth, {})[:pass]
|
|
|
|
end
|
|
|
|
|
|
|
|
def request_headers
|
|
|
|
opts.fetch(:headers, {})
|
|
|
|
end
|
|
|
|
|
|
|
|
def request_body
|
|
|
|
opts[:data]
|
|
|
|
end
|
|
|
|
|
|
|
|
def open_timeout
|
|
|
|
opts.fetch(:open_timeout, 60)
|
|
|
|
end
|
|
|
|
|
|
|
|
def read_timeout
|
|
|
|
opts.fetch(:read_timeout, 60)
|
|
|
|
end
|
|
|
|
|
|
|
|
def ssl_verify?
|
|
|
|
opts.fetch(:ssl_verify, true)
|
|
|
|
end
|
2018-10-13 06:14:17 +00:00
|
|
|
|
|
|
|
def max_redirects
|
|
|
|
opts.fetch(:max_redirects, 0)
|
|
|
|
end
|
2017-10-04 20:44:09 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
class Local < Base
|
|
|
|
def status
|
|
|
|
response.status
|
|
|
|
end
|
|
|
|
|
|
|
|
def body
|
|
|
|
response.body
|
|
|
|
end
|
|
|
|
|
|
|
|
def response_headers
|
|
|
|
response.headers.to_h
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def response
|
|
|
|
return @response if @response
|
2018-10-13 06:14:17 +00:00
|
|
|
conn = Faraday.new(url: url, headers: request_headers, params: params, ssl: { verify: ssl_verify? }) do |builder|
|
|
|
|
builder.request :url_encoded
|
|
|
|
builder.use FaradayMiddleware::FollowRedirects, limit: max_redirects if max_redirects > 0
|
|
|
|
builder.adapter Faraday.default_adapter
|
|
|
|
end
|
2017-10-04 20:44:09 +00:00
|
|
|
|
|
|
|
# set basic authentication
|
|
|
|
conn.basic_auth username, password unless username.nil? || password.nil?
|
|
|
|
|
|
|
|
# set default timeout
|
|
|
|
conn.options.timeout = read_timeout # open/read timeout in seconds
|
|
|
|
conn.options.open_timeout = open_timeout # connection open timeout in seconds
|
|
|
|
|
2018-02-27 17:59:53 +00:00
|
|
|
@response = conn.run_request(http_method.downcase.to_sym, nil, nil, nil) do |req|
|
2017-10-04 20:44:09 +00:00
|
|
|
req.body = request_body
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Remote < Base
|
|
|
|
attr_reader :inspec
|
|
|
|
|
|
|
|
def initialize(inspec, http_method, url, opts)
|
2018-02-13 17:42:16 +00:00
|
|
|
unless inspec.command('curl').exist?
|
|
|
|
raise Inspec::Exceptions::ResourceSkipped,
|
|
|
|
'curl is not available on the target machine'
|
|
|
|
end
|
|
|
|
|
2018-03-28 15:22:01 +00:00
|
|
|
@ran_curl = false
|
2017-10-04 20:44:09 +00:00
|
|
|
@inspec = inspec
|
|
|
|
super(http_method, url, opts)
|
|
|
|
end
|
|
|
|
|
|
|
|
def status
|
|
|
|
run_curl
|
|
|
|
@status
|
|
|
|
end
|
|
|
|
|
|
|
|
def body
|
|
|
|
run_curl
|
2017-11-16 17:16:23 +00:00
|
|
|
@body&.strip
|
2017-10-04 20:44:09 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def response_headers
|
|
|
|
run_curl
|
|
|
|
@response_headers
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def run_curl
|
|
|
|
return if @ran_curl
|
|
|
|
|
2018-03-08 04:04:26 +00:00
|
|
|
cmd_result = inspec.command(curl_command)
|
|
|
|
response = cmd_result.stdout
|
2017-10-04 20:44:09 +00:00
|
|
|
@ran_curl = true
|
2018-03-08 04:04:26 +00:00
|
|
|
return if response.nil? || cmd_result.exit_status != 0
|
2017-10-04 20:44:09 +00:00
|
|
|
|
|
|
|
# strip any carriage returns to normalize output
|
|
|
|
response.delete!("\r")
|
|
|
|
|
|
|
|
# split the prelude (status line and headers) and the body
|
2018-10-13 06:14:17 +00:00
|
|
|
prelude, remainder = response.split("\n\n", 2)
|
|
|
|
loop do
|
|
|
|
break unless remainder =~ %r{^HTTP/}
|
|
|
|
prelude, remainder = remainder.split("\n\n", 2)
|
|
|
|
end
|
|
|
|
@body = remainder
|
2017-10-04 20:44:09 +00:00
|
|
|
prelude = prelude.lines
|
|
|
|
|
|
|
|
# grab the status off of the first line of the prelude
|
|
|
|
status_line = prelude.shift
|
|
|
|
@status = status_line.split(' ', 3)[1].to_i
|
|
|
|
|
|
|
|
# parse the rest of the prelude which will be all the HTTP headers
|
|
|
|
@response_headers = {}
|
|
|
|
prelude.each do |line|
|
|
|
|
line.strip!
|
|
|
|
key, value = line.split(':', 2)
|
|
|
|
@response_headers[key] = value.strip
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-11-16 17:16:23 +00:00
|
|
|
def curl_command # rubocop:disable Metrics/AbcSize
|
2017-11-27 17:17:39 +00:00
|
|
|
cmd = ['curl -i']
|
|
|
|
|
|
|
|
# Use curl's --head option when the method requested is HEAD. Otherwise,
|
|
|
|
# the user may experience a timeout when curl does not properly close
|
|
|
|
# the connection after the response is received.
|
|
|
|
if http_method.casecmp('HEAD') == 0
|
|
|
|
cmd << '--head'
|
|
|
|
else
|
|
|
|
cmd << "-X #{http_method}"
|
|
|
|
end
|
|
|
|
|
2017-10-04 20:44:09 +00:00
|
|
|
cmd << "--connect-timeout #{open_timeout}"
|
2017-11-09 10:11:19 +00:00
|
|
|
cmd << "--max-time #{open_timeout+read_timeout}"
|
2017-10-04 20:44:09 +00:00
|
|
|
cmd << "--user \'#{username}:#{password}\'" unless username.nil? || password.nil?
|
|
|
|
cmd << '--insecure' unless ssl_verify?
|
|
|
|
cmd << "--data #{Shellwords.shellescape(request_body)}" unless request_body.nil?
|
2018-10-13 06:14:17 +00:00
|
|
|
cmd << '--location' if max_redirects > 0
|
|
|
|
cmd << "--max-redirs #{max_redirects}" if max_redirects > 0
|
2017-02-02 21:13:03 +00:00
|
|
|
|
2017-10-04 20:44:09 +00:00
|
|
|
request_headers.each do |k, v|
|
2017-11-03 18:28:54 +00:00
|
|
|
cmd << "-H '#{k}: #{v}'"
|
2017-10-04 20:44:09 +00:00
|
|
|
end
|
2017-02-02 21:13:03 +00:00
|
|
|
|
2017-11-16 17:16:23 +00:00
|
|
|
if params.nil?
|
|
|
|
cmd << "'#{url}'"
|
|
|
|
else
|
|
|
|
cmd << "'#{url}?#{params.map { |e| e.join('=') }.join('&')}'"
|
|
|
|
end
|
2017-02-02 21:13:03 +00:00
|
|
|
|
2017-10-04 20:44:09 +00:00
|
|
|
cmd.join(' ')
|
|
|
|
end
|
2017-02-02 21:13:03 +00:00
|
|
|
end
|
2017-01-05 19:29:11 +00:00
|
|
|
end
|
2018-01-16 22:30:35 +00:00
|
|
|
|
|
|
|
class Headers < Hash
|
|
|
|
def self.create(header_data)
|
|
|
|
header_data.each_with_object(new) { |(k, v), memo| memo[k.to_s.downcase] = v }
|
|
|
|
end
|
|
|
|
|
|
|
|
def [](requested_key)
|
|
|
|
fetch(requested_key.downcase, nil)
|
|
|
|
end
|
|
|
|
|
|
|
|
def method_missing(requested_key)
|
|
|
|
fetch(requested_key.to_s.downcase, nil)
|
|
|
|
end
|
|
|
|
end
|
2017-01-05 19:29:11 +00:00
|
|
|
end
|
|
|
|
end
|