Allow http resource to follow redirects

By specifying a `max_redirects` attribute, the `http` resource worker
will follow any HTTP Redirect response (301, 302, etc...) up to the
limit defined by this attribute. For a local worker, exceeding that
limit will raise a `FaradayMiddleware::RedirectLimitReached` exception.
For a remote worker, the curl command will exit without populating the
`status` and `body` properties.

Signed-off-by: Keith Walters <keith.walters@cattywamp.us>
This commit is contained in:
Keith Walters 2018-10-13 02:14:17 -04:00
parent 9c67fe89a5
commit c2bd0616fe
6 changed files with 84 additions and 4 deletions

View file

@ -23,7 +23,7 @@ This resource first became available in v1.10.0 of InSpec.
An `http` resource block declares the configuration settings to be tested:
describe http('url', auth: {user: 'user', pass: 'test'}, params: {params}, method: 'method', headers: {headers}, data: data, open_timeout: 60, read_timeout: 60, ssl_verify: true) do
describe http('url', auth: {user: 'user', pass: 'test'}, params: {params}, method: 'method', headers: {headers}, data: data, open_timeout: 60, read_timeout: 60, ssl_verify: true, max_redirects: 3) do
its('status') { should eq number }
its('body') { should eq 'body' }
its('headers.name') { should eq 'header' }
@ -40,6 +40,7 @@ where
* `open_timeout` may be specified for a timeout for opening connections (default to 60)
* `read_timeout` may be specified for a timeout for reading connections (default to 60)
* `ssl_verify` may be specified to enable or disable verification of SSL certificates (default to `true`)
* `max_redirects` may be specified to control how many HTTP Redirects to follow (defaults to `0`)
<br>
@ -84,7 +85,7 @@ In InSpec 2.0, the HTTP test will automatically execute remotely whenever InSpec
## Parameters
* `url`, `auth`, `params`, `method`, `headers`, `data`, `open_timeout`, `read_timeout`, `ssl_verify`
* `url`, `auth`, `params`, `method`, `headers`, `data`, `open_timeout`, `read_timeout`, `ssl_verify`, `max_redirects`
## Parameter Examples
@ -170,6 +171,17 @@ In InSpec 2.0, the HTTP test will automatically execute remotely whenever InSpec
<br>
### max_redirects
`max_redirects` may be specified to control how many HTTP Redirects to follow (default to 0).
describe http('http://localhost:8080/ping',
max_redirects: 3) do
...
end
<br>
## Properties
* `body`, `headers`, `http_method`, `status`,

View file

@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'sslshake', '~> 1.2'
spec.add_dependency 'parallel', '~> 1.9'
spec.add_dependency 'faraday', '>=0.9.0'
spec.add_dependency 'faraday_middleware', '~> 0.12.2'
spec.add_dependency 'tomlrb', '~> 1.2'
spec.add_dependency 'addressable', '~> 2.4'
spec.add_dependency 'parslet', '~> 1.5'

View file

@ -4,6 +4,7 @@
# license: Apache v2
require 'faraday'
require 'faraday_middleware'
require 'hashie'
module Inspec::Resources
@ -114,6 +115,10 @@ module Inspec::Resources
def ssl_verify?
opts.fetch(:ssl_verify, true)
end
def max_redirects
opts.fetch(:max_redirects, 0)
end
end
class Local < Base
@ -133,7 +138,11 @@ module Inspec::Resources
def response
return @response if @response
conn = Faraday.new url: url, headers: request_headers, params: params, ssl: { verify: ssl_verify? }
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
# set basic authentication
conn.basic_auth username, password unless username.nil? || password.nil?
@ -191,7 +200,12 @@ module Inspec::Resources
response.delete!("\r")
# split the prelude (status line and headers) and the body
prelude, @body = response.split("\n\n", 2)
prelude, remainder = response.split("\n\n", 2)
loop do
break unless remainder =~ %r{^HTTP/}
prelude, remainder = remainder.split("\n\n", 2)
end
@body = remainder
prelude = prelude.lines
# grab the status off of the first line of the prelude
@ -224,6 +238,8 @@ module Inspec::Resources
cmd << "--user \'#{username}:#{password}\'" unless username.nil? || password.nil?
cmd << '--insecure' unless ssl_verify?
cmd << "--data #{Shellwords.shellescape(request_body)}" unless request_body.nil?
cmd << '--location' if max_redirects > 0
cmd << "--max-redirs #{max_redirects}" if max_redirects > 0
request_headers.each do |k, v|
cmd << "-H '#{k}: #{v}'"

View file

@ -515,6 +515,7 @@ class MockLoader
# http resource - remote worker'
%{bash -c 'type "curl"'} => cmd.call('bash-c-type-curl'),
"curl -i -X GET --connect-timeout 60 --max-time 120 'http://www.example.com'" => cmd.call('http-remote-no-options'),
"curl -i -X GET --connect-timeout 60 --max-time 120 --location --max-redirs 1 'http://www.example.com'" => cmd.call('http-remote-max-redirs'),
"curl -i -X GET --connect-timeout 60 --max-time 120 --user 'user:pass' 'http://www.example.com'" => cmd.call('http-remote-basic-auth'),
'f77ebcedaf6fbe8f02d2f9d4735a90c12311d2ca4b43ece9efa2f2e396491747' => cmd.call('http-remote-post'),
"curl -i -X GET --connect-timeout 60 --max-time 120 -H 'accept: application/json' -H 'foo: bar' 'http://www.example.com'" => cmd.call('http-remote-headers'),

View file

@ -0,0 +1,21 @@
HTTP/1.1 301 Moved Permanently
Date: Sat, 13 Oct 2018 05:06:12 GMT
Content-Type: text/html
Content-Length: 186
Connection: keep-alive
Server: gws
Location: http://example.com
HTTP/1.1 200 OK
Date: Sat, 13 Oct 2018 05:06:12 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=ISO-8859-1
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Accept-Ranges: none
Vary: Accept-Encoding
Transfer-Encoding: chunked
followed redirect

View file

@ -31,6 +31,26 @@ describe 'Inspec::Resources::Http' do
end
end
describe 'request with redirect enabled' do
let(:opts) { { max_redirects: 1 } }
it 'follows the redirect' do
stub_request(:get, domain).to_return(status: 302, headers: { location: 'http://example.com' } )
stub_request(:get, 'example.com').to_return(status: 200, body: 'redirect ok')
_(worker.status).must_equal 200
_(worker.body).must_equal 'redirect ok'
end
it 'does not exceed max_redirects' do
stub_request(:get, domain).to_return(status: 302, headers: { location: 'http://redirect1.com' } )
stub_request(:get, 'redirect1.com').to_return(status: 302, headers: { location: 'http://redirect2.com' } )
stub_request(:get, 'redirect2.com').to_return(status: 200, body: 'should not get here')
proc { worker.status }.must_raise FaradayMiddleware::RedirectLimitReached
end
end
describe 'POST request with data' do
let(:http_method) { 'POST'}
let(:opts) { { data: {a: '1', b: 'five'} } }
@ -109,6 +129,15 @@ describe 'Inspec::Resources::Http' do
end
end
describe 'request with redirect enabled' do
let(:opts) { { max_redirects: 1 } }
it 'follows the redirect' do
_(worker.status).must_equal 200
_(worker.body).must_equal 'followed redirect'
end
end
describe 'POST request with data' do
let(:http_method) { 'POST'}
let(:opts) { { data: {a: '1', b: 'five'} } }