Skip to content

Commit

Permalink
Fix etag 304 (#777)
Browse files Browse the repository at this point in the history
* Properly treat caching headers (#716)

* Properly treat caching headers

* Make rubocop happy

* Add proper http error handling to requestforwarder

* Make forwarder work even without disposition (#771)

* Fix tests
  • Loading branch information
ShimShtein authored Nov 8, 2022
1 parent 532e171 commit 314f82b
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,33 @@ def forward_request
}, status: :bad_gateway
end

if @cloud_response.headers[:content_disposition]
return send_data @cloud_response, disposition: @cloud_response.headers[:content_disposition], type: @cloud_response.headers[:content_type]
if @cloud_response.code >= 300
return render json: {
:message => 'Cloud request failed',
:headers => {},
:response => @cloud_response,
}, status: @cloud_response.code
end

# Append redhat-specific headers
@cloud_response.headers.each do |key, value|
assign_header(response, @cloud_response, key, false) if key.to_s.start_with?('x_rh_')
end
# Append general headers
assign_header(response, @cloud_response, :x_resource_count, true)
assign_header(response, @cloud_response, :x_rh_insights_request_id, false)
headers[Rack::ETAG] = @cloud_response.headers[:etag]

render json: @cloud_response, status: @cloud_response.code
if @cloud_response.headers[:content_disposition]
# If there is a Content-Disposition header, it means we are forwarding binary data, send the raw data with proper
# content type
send_data @cloud_response, disposition: @cloud_response.headers[:content_disposition], type: @cloud_response.headers[:content_type]
elsif @cloud_response.headers[:content_type] =~ /zip/
# if there is no Content-Disposition, but the content type is binary according the content type,
# forward the request as binry too
send_data @cloud_response, type: @cloud_response.headers[:content_type]
else
render json: @cloud_response, status: @cloud_response.code
end
end

def branch_info
Expand Down
23 changes: 18 additions & 5 deletions app/services/foreman_rh_cloud/cloud_request_forwarder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@ def forward_request(original_request, controller_name, branch_id, certs)
logger.debug("Sending request to: #{request_opts[:url]}")

execute_cloud_request(request_opts)
rescue RestClient::Exception => error_response
error_response.response
end

def prepare_request_opts(original_request, forward_payload, forward_params, certs)
base_params = {
method: original_request.method,
payload: forward_payload,
headers: {
params: forward_params,
user_agent: http_user_agent(original_request),
content_type: original_request.media_type.presence || original_request.format.to_s,
},
headers: original_headers(original_request).merge(
{
params: forward_params,
user_agent: http_user_agent(original_request),
content_type: original_request.media_type.presence || original_request.format.to_s,
}),
}
base_params.merge(path_params(original_request.path, certs))
end
Expand Down Expand Up @@ -78,6 +81,16 @@ def path_params(request_path, certs)
end
end

def original_headers(original_request)
headers = {
if_none_match: original_request.if_none_match,
if_modified_since: original_request.if_modified_since,
}.compact

logger.debug("Sending headers: #{headers}")
headers
end

def platform_request?
->(request_path) { request_path.include? '/platform' }
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
module InsightsCloud::Api
class MachineTelemetriesControllerTest < ActionController::TestCase
context '#forward_request' do
include MockCerts

setup do
@body = 'Cloud response body'
@http_req = RestClient::Request.new(:method => 'GET', :url => 'http://test.theforeman.org')
Expand All @@ -12,38 +14,9 @@ class MachineTelemetriesControllerTest < ActionController::TestCase
User.current = ::Katello::CpConsumerUser.new(:uuid => host.subscription_facet.uuid, :login => host.subscription_facet.uuid)
InsightsCloud::Api::MachineTelemetriesController.any_instance.stubs(:upstream_owner).returns({ 'uuid' => 'abcdefg' })

@cert1 = "-----BEGIN CERTIFICATE-----\r\n" +
"MIIFdDCCA1ygAwIBAgIJAM5Uqykb3EAtMA0GCSqGSIb3DQEBCwUAME8xCzAJBgNV\r\n" +
"BAYTAklMMREwDwYDVQQIDAhUZWwgQXZpdjEUMBIGA1UECgwLVGhlIEZvcmVtYW4x\r\n" +
"FzAVBgNVBAMMDnRoZWZvcmVtYW4ub3JnMB4XDTE4MDMyNDEyMzYyOFoXDTI4MDMy\r\n" +
"MTEyMzYyOFowTzELMAkGA1UEBhMCSUwxETAPBgNVBAgMCFRlbCBBdml2MRQwEgYD\r\n" +
"VQQKDAtUaGUgRm9yZW1hbjEXMBUGA1UEAwwOdGhlZm9yZW1hbi5vcmcwggIiMA0G\r\n" +
"CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF04/s4h+BgHPG1HDZ/sDlYq925pkc\r\n" +
"RTVAfnE2EXDAmZ6W4Q9ueDY65MHe3ZWO5Dg72kNSP2sK9kRI7Dk5CAFOgyw1rH8t\r\n" +
"Hd1+0xp/lv6e4SvSYghxIL68vFe0ftKkm1usqejBM5ZTgKr7JCI+XSIN36F65Kde\r\n" +
"c+vxwBnayuhP04r9/aaE/709SXML4eRVYW8I3qFy9FPtUOm+bY8U2PIv5fHayqbG\r\n" +
"cL/4t3+MCtMhHJsLzdBXya+1P5t+HcKjUNlmwoUF961YAktVuEFloGd0RMRlqF3/\r\n" +
"itU3QNlXgA5QBIciE5VPr/PiqgMC3zgd5avjF4OribZ+N9AATLiQMW78il5wSfcc\r\n" +
"kQjU9ChOLrzku455vQ8KE4bc0qvpCWGfUah6MvL9JB+TQkRl/8kxl0b9ZinIvJDH\r\n" +
"ynVMb4cB/TDEjrjOfzn9mWLH0ZJqjmc2bER/G12WQxOaYLxdVwRStD3Yh6PtiFWu\r\n" +
"sXOk19UOTVkeuvGFVtvzLfEwQ1lDEo7+VBQz8FG/HBu2Hpq3IwCFrHuicikwjQJk\r\n" +
"nfturgD0rBOKEc1qWNZRCvovYOLL6ihvv5Orujsx5ZCHOAtnVNxkvIlFt2RS45LF\r\n" +
"MtPJyhAc6SjitllfUEirxprsbmeSZqrIfzcGaEhgOSnyik1WMv6bYiqPfBg8Fzjh\r\n" +
"vOCbtiDNPmvgOwIDAQABo1MwUTAdBgNVHQ4EFgQUtkAgQopsTtG9zSG3MgW2IxHD\r\n" +
"MDwwHwYDVR0jBBgwFoAUtkAgQopsTtG9zSG3MgW2IxHDMDwwDwYDVR0TAQH/BAUw\r\n" +
"AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAJq7iN+ZroRBweNhvUobxs75bLIV6tNn1\r\n" +
"MdNHDRA+hezwf+gxHZhFyaAHfTpst2/9leK5Qe5Zd6gZLr3E5/8ppQuRod72H39B\r\n" +
"vxMlG5zxDss0WMo3vZeKZbTY6QhXi/lY2IZ6OGV4feSvCsYxn27GTjjrRUSLFeHH\r\n" +
"JVemCwCDMavaE3+OIY4v2P4FcG+MjUvfOB9ahI24TWL7YgrsNVmJjCILq+EeUj0t\r\n" +
"Gde1SXVyLkqt7PoxHRJAE0BCEMJSnjxaVB329acJgeehBUxjj4CCPqtDxtbz9HEH\r\n" +
"mOKfNdaKpFor+DUeEKUWVGnr9U9xOaC+Ws+oX7MIEUCDM7p2ob4JwcjnFs1jZgHh\r\n" +
"Hwig+i7doTlc701PvKWO96fuNHK3B3/jTb1fVvSZ49O/RvY1VWODdUdxWmXGHNh3\r\n" +
"LoR8tSPEb46lC2DXGaIQumqQt8PnBG+vL1qkQa1SGTV7dJ8TTbxbv0S+sS+igkk9\r\n" +
"zsIEK8Ea3Ep935cXximz0faAAKHSA+It+xHLAyDtqy2KaAEBgGsBuuWlUfK6TaP3\r\n" +
"Gwdjct3y4yYUO45lUsUfHqX8vk/4ttW5zYeDiW+HArJz+9VUXNbEdury4kGuHgBj\r\n" +
"xHD4Bsul65+hHZ9QywKU26F1A6TLkYpQ2rk/Dx9LGICM4m4IlHjWJPFsQdtkyOor\r\n" +
"osxMtcaZZ1E=\r\n" +
"-----END CERTIFICATE-----"
setup_certs_expectation do
InsightsCloud::Api::MachineTelemetriesController.any_instance.stubs(:candlepin_id_cert)
end
end

test "should respond with response from cloud" do
Expand All @@ -63,13 +36,6 @@ class MachineTelemetriesControllerTest < ActionController::TestCase
::ForemanRhCloud::CloudRequestForwarder.any_instance.expects(:execute_cloud_request).with do |opts|
opts[:headers][:content_type] == 'application/json'
end.returns(res)
InsightsCloud::Api::MachineTelemetriesController.any_instance.expects(:candlepin_id_cert)
.returns(
{
cert: @cert1,
key: OpenSSL::PKey::RSA.new(1024).to_pem,
}
)
InsightsCloud::Api::MachineTelemetriesController.any_instance.expects(:cp_owner_id).returns('123')

post :forward_request, as: :json, params: { "path" => "static/v1/test", "machine_telemetry" => {"foo" => "bar"} }
Expand All @@ -90,6 +56,29 @@ class MachineTelemetriesControllerTest < ActionController::TestCase
assert_equal x_rh_insights_request_id, @response.headers['x_rh_insights_request_id']
end

test "should set etag header to response from cloud" do
etag = '12345'
req = RestClient::Request.new(:method => 'GET', :url => 'http://test.theforeman.org', :headers => { "If-None-Match": etag})
net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
net_http_resp[Rack::ETAG] = etag
res = RestClient::Response.create(@body, net_http_resp, req)
::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:forward_request).returns(res)

get :forward_request, params: { "path" => "static/v1/release/insights-core.egg" }
assert_equal etag, @response.headers[Rack::ETAG]
end

test "should set content type header to response from cloud" do
req = RestClient::Request.new(:method => 'GET', :url => 'http://test.theforeman.org')
net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
net_http_resp['Content-Type'] = 'application/zip'
res = RestClient::Response.create(@body, net_http_resp, req)
::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:forward_request).returns(res)

get :forward_request, params: { "path" => "static/v1/release/insights-core.egg" }
assert_equal net_http_resp['Content-Type'], @response.headers['Content-Type']
end

test "should handle failed authentication to cloud" do
net_http_resp = Net::HTTPResponse.new(1.0, 401, "Unauthorized")
res = RestClient::Response.create(@body, net_http_resp, @http_req)
Expand All @@ -99,6 +88,17 @@ class MachineTelemetriesControllerTest < ActionController::TestCase
assert_equal 502, @response.status
assert_equal 'Authentication to the Insights Service failed.', JSON.parse(@response.body)['message']
end

test "should forward errors to the client" do
net_http_resp = Net::HTTPResponse.new(1.0, 500, "TEST_RESPONSE")
res = RestClient::Response.create(@body, net_http_resp, @http_req)
::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:execute_cloud_request).raises(RestClient::InternalServerError.new(res))

get :forward_request, params: { "path" => "platform/module-update-router/v1/channel" }
assert_equal 500, @response.status
assert_equal 'Cloud request failed', JSON.parse(@response.body)['message']
assert_match /#{@body}/, JSON.parse(@response.body)['response']
end
end

context '#branch_info' do
Expand Down
53 changes: 53 additions & 0 deletions test/test_plugin_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,56 @@ module KatelloLocationFix
end
end
end

module MockCerts
extend ActiveSupport::Concern

def test_certificate
@test_certificate ||= "-----BEGIN CERTIFICATE-----\r\n" +
"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\r\n" +
"osxMtcaZZ1E=\r\n" +
"-----END CERTIFICATE-----"
end

def generate_certs_hash
{
cert: test_certificate,
key: OpenSSL::PKey::RSA.new(1024).to_pem,
}
end

def setup_certs_expectation
expectation = yield
expectation.returns(
generate_certs_hash
)
end
end

0 comments on commit 314f82b

Please sign in to comment.