inspec/lib/resources/http.rb
Adam Leff 98db74a466 http resource: properly support HEAD request with remote worker (#2340)
The existing method of adding `-X HEAD` to the curl command does not
work properly and can cause timeouts because curl doesn't properly
close the connection. The correct way is to use curl's own `--head`
flag.

Signed-off-by: Adam Leff <adam@leff.co>
2017-11-27 18:17:39 +01:00

238 lines
6.1 KiB
Ruby

# encoding: utf-8
# copyright: 2017, Criteo
# copyright: 2017, Chef Software Inc
# author: Guilhem Lettron, Christoph Hartmann
# license: Apache v2
require 'faraday'
require 'hashie'
module Inspec::Resources
class Http < Inspec.resource(1)
name 'http'
desc 'Use the http InSpec audit resource to test http call.'
example "
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' }
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' }
end
# properly execute the HTTP call on the scanned machine instead of the
# machine executing InSpec. This will be the default behavior in InSpec 2.0.
describe http('http://localhost:8080', enable_remote_worker: true) do
its('body') { should cmp 'local web server on target machine' }
end
"
def initialize(url, opts = {})
@url = url
@opts = opts
if use_remote_worker?
return skip_resource 'curl is not available on the target machine' unless inspec.command('curl').exist?
@worker = Worker::Remote.new(inspec, http_method, url, opts)
else
@worker = Worker::Local.new(http_method, url, opts)
end
end
def status
@worker.status
end
def headers
Hashie::Mash.new(@worker.response_headers)
end
def body
@worker.body
end
def http_method
@opts.fetch(:method, 'GET')
end
def to_s
"http #{http_method} on #{@url}"
end
private
def use_remote_worker?
return false if inspec.local_transport?
return true if @opts[:enable_remote_worker]
warn "[DEPRECATION] #{self} will execute locally instead of the target machine. To execute remotely, add `enable_remote_worker: true`."
warn '[DEPRECATION] `enable_remote_worker: true` will be the default behavior in InSpec 2.0.'
false
end
class Worker
class Base
attr_reader :http_method, :opts, :url
def initialize(http_method, url, opts)
@http_method = http_method
@url = url
@opts = opts
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
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
conn = Faraday.new url: url, headers: request_headers, params: params, ssl: { verify: ssl_verify? }
# 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
@response = conn.send(http_method.downcase) do |req|
req.body = request_body
end
end
end
class Remote < Base
attr_reader :inspec
def initialize(inspec, http_method, url, opts)
@inspec = inspec
super(http_method, url, opts)
end
def status
run_curl
@status
end
def body
run_curl
@body&.strip
end
def response_headers
run_curl
@response_headers
end
private
def run_curl
return if @ran_curl
response = inspec.command(curl_command).stdout
@ran_curl = true
return if response.nil?
# strip any carriage returns to normalize output
response.delete!("\r")
# split the prelude (status line and headers) and the body
prelude, @body = response.split("\n\n", 2)
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
def curl_command # rubocop:disable Metrics/AbcSize
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
cmd << "--connect-timeout #{open_timeout}"
cmd << "--max-time #{open_timeout+read_timeout}"
cmd << "--user \'#{username}:#{password}\'" unless username.nil? || password.nil?
cmd << '--insecure' unless ssl_verify?
cmd << "--data #{Shellwords.shellescape(request_body)}" unless request_body.nil?
request_headers.each do |k, v|
cmd << "-H '#{k}: #{v}'"
end
if params.nil?
cmd << "'#{url}'"
else
cmd << "'#{url}?#{params.map { |e| e.join('=') }.join('&')}'"
end
cmd.join(' ')
end
end
end
end
end