mirror of
https://github.com/inspec/inspec
synced 2024-11-14 00:47:10 +00:00
6be9f32448
* http resource: make header keys case insensitive HTTP header keys are currently case-sensitive, and the local and remote workers currently store the keys in different formats due to the different tools generating them. This change ensures the ability to fetch headers by key is case-insensitive and adds a deprecation to inform the user that future fetches should use all lowercase. Signed-off-by: Adam Leff <adam@leff.co> * Remove deprecation treatment HTTP header keys are case insensitive anyways. There's no reason to make things harder for our users. Signed-off-by: Adam Leff <adam@leff.co>
252 lines
6.5 KiB
Ruby
252 lines
6.5 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
|
|
@headers ||= Inspec::Resources::Http::Headers.create(@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
|
|
|
|
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
|
|
end
|
|
end
|