Skip to content

add ability to use http proxy when making web requests #242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Lint/HandleExceptions:
# Offense count: 8
# Configuration parameters: CountKeywordArgs.
Metrics/ParameterLists:
Max: 13
Max: 14

# Offense count: 2
Naming/AccessorMethodName:
Expand Down
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
install:
- npm i -g markdown-spellcheck
before_script:
# todo: change branch to master once merged.
- wget --quiet https://raw.githubusercontent.com/optimizely/mdspell-config/master/.spelling
script:
- mdspell -a -n -r --en-us '**/*.md'
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ The `HTTPConfigManager` asynchronously polls for datafiles from a specified URL
error_handler: nil,
skip_json_validation: false,
notification_center: notification_center,
datafile_access_token: nil
datafile_access_token: nil,
proxy_config: nil
)
~~~~~~
**Note:** You must provide either the `sdk_key` or URL. If you provide both, the URL takes precedence.
Expand Down
34 changes: 34 additions & 0 deletions lib/optimizely/config/proxy_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

# Copyright 2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#

module Optimizely
class ProxyConfig
attr_reader :host, :port, :username, :password

def initialize(host, port = nil, username = nil, password = nil)
# host - DNS name or IP address of proxy
# port - port to use to acess the proxy
# username - username if authorization is required
# password - password if authorization is required
@host = host
@port = port
@username = username
@password = password
end
end
end
7 changes: 5 additions & 2 deletions lib/optimizely/config_manager/http_project_config_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class HTTPProjectConfigManager < ProjectConfigManager
# skip_json_validation - Optional boolean param which allows skipping JSON schema
# validation upon object invocation. By default JSON schema validation will be performed.
# datafile_access_token - access token used to fetch private datafiles
# proxy_config - Optional proxy config instancea to configure making web requests through a proxy server.
def initialize(
sdk_key: nil,
url: nil,
Expand All @@ -65,7 +66,8 @@ def initialize(
error_handler: nil,
skip_json_validation: false,
notification_center: nil,
datafile_access_token: nil
datafile_access_token: nil,
proxy_config: nil
)
@logger = logger || NoOpLogger.new
@error_handler = error_handler || NoOpErrorHandler.new
Expand All @@ -86,6 +88,7 @@ def initialize(
# Start async scheduler in the end to avoid race condition where scheduler executes
# callback which makes use of variables not yet initialized by the main thread.
@async_scheduler.start! if start_by_default == true
@proxy_config = proxy_config
@stopped = false
end

Expand Down Expand Up @@ -161,7 +164,7 @@ def request_config

begin
response = Helpers::HttpUtils.make_request(
@datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT']
@datafile_url, :get, nil, headers, Helpers::Constants::CONFIG_MANAGER['REQUEST_TIMEOUT'], @proxy_config
)
rescue StandardError => e
@logger.log(
Expand Down
5 changes: 3 additions & 2 deletions lib/optimizely/event_dispatcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,18 @@ class EventDispatcher
# @api constants
REQUEST_TIMEOUT = 10

def initialize(logger: nil, error_handler: nil)
def initialize(logger: nil, error_handler: nil, proxy_config: nil)
@logger = logger || NoOpLogger.new
@error_handler = error_handler || NoOpErrorHandler.new
@proxy_config = proxy_config
end

# Dispatch the event being represented by the Event object.
#
# @param event - Event object
def dispatch_event(event)
response = Helpers::HttpUtils.make_request(
event.url, event.http_verb, event.params.to_json, event.headers, REQUEST_TIMEOUT
event.url, event.http_verb, event.params.to_json, event.headers, REQUEST_TIMEOUT, @proxy_config
)

error_msg = "Event failed to dispatch with response code: #{response.code}"
Expand Down
23 changes: 17 additions & 6 deletions lib/optimizely/helpers/http_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,10 @@ module Helpers
module HttpUtils
module_function

def make_request(url, http_method, request_body = nil, headers = {}, read_timeout = nil)
def make_request(url, http_method, request_body = nil, headers = {}, read_timeout = nil, proxy_config = nil)
# makes http/https GET/POST request and returns response

#
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)

http.read_timeout = read_timeout if read_timeout
http.use_ssl = uri.scheme == 'https'

if http_method == :get
request = Net::HTTP::Get.new(uri.request_uri)
Expand All @@ -46,6 +42,21 @@ def make_request(url, http_method, request_body = nil, headers = {}, read_timeou
request[key] = val
end

# do not try to make request with proxy unless we have at least a host
http_class = if proxy_config&.host
Net::HTTP::Proxy(
proxy_config.host,
proxy_config.port,
proxy_config.username,
proxy_config.password
)
else
Net::HTTP
end

http = http_class.new(uri.host, uri.port)
http.read_timeout = read_timeout if read_timeout
http.use_ssl = uri.scheme == 'https'
http.request(request)
end
end
Expand Down
44 changes: 44 additions & 0 deletions spec/config/proxy_config_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

#
# Copyright 2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'spec_helper'
require 'optimizely/config/proxy_config'

describe Optimizely::ProxyConfig do
let(:host) { 'host' }
let(:port) { 1234 }
let(:username) { 'username' }
let(:password) { 'password' }

describe '#initialize' do
it 'defines getters for host, port, username, and password' do
proxy_config = described_class.new(host, port, username, password)

expect(proxy_config.host).to eq(host)
expect(proxy_config.port).to eq(port)
expect(proxy_config.username).to eq(username)
expect(proxy_config.password).to eq(password)
end

it 'sets port, username, and password to nil if they are not passed in' do
proxy_config = described_class.new(host)
expect(proxy_config.port).to eq(nil)
expect(proxy_config.username).to eq(nil)
expect(proxy_config.password).to eq(nil)
end
end
end
15 changes: 14 additions & 1 deletion spec/config_manager/http_project_config_manager_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@
datafile_access_token: 'the-token'
)
sleep 0.1
expect(Optimizely::Helpers::HttpUtils).to have_received(:make_request).with(anything, anything, anything, hash_including('Authorization' => 'Bearer the-token'), anything)
expect(Optimizely::Helpers::HttpUtils).to have_received(:make_request).with(anything, anything, anything, hash_including('Authorization' => 'Bearer the-token'), anything, anything)
end

it 'should use authenticated datafile url when auth token is provided' do
Expand Down Expand Up @@ -526,5 +526,18 @@
sleep 0.1
expect(spy_logger).to have_received(:log).with(Logger::DEBUG, 'Datafile request headers: {"Content-Type"=>"application/json", "Authorization"=>"********"}').once
end

it 'should pass the proxy config that is passed in' do
proxy_config = double(:proxy_config)

allow(Optimizely::Helpers::HttpUtils).to receive(:make_request)
@http_project_config_manager = Optimizely::HTTPProjectConfigManager.new(
sdk_key: 'valid_sdk_key',
datafile_access_token: 'the-token',
proxy_config: proxy_config
)
sleep 0.1
expect(Optimizely::Helpers::HttpUtils).to have_received(:make_request).with(anything, anything, anything, hash_including('Authorization' => 'Bearer the-token'), anything, proxy_config)
end
end
end
21 changes: 20 additions & 1 deletion spec/event_dispatcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
describe Optimizely::EventDispatcher do
let(:error_handler) { spy(Optimizely::NoOpErrorHandler.new) }
let(:spy_logger) { spy('logger') }
let(:proxy_config) { nil }

before(:context) do
@url = 'https://www.optimizely.com'
Expand All @@ -37,10 +38,28 @@
before(:example) do
@event_dispatcher = Optimizely::EventDispatcher.new
@customized_event_dispatcher = Optimizely::EventDispatcher.new(
logger: spy_logger, error_handler: error_handler
logger: spy_logger, error_handler: error_handler, proxy_config: proxy_config
)
end

context 'passing in proxy config' do
let(:proxy_config) { double(:proxy_config) }

it 'should pass the proxy_config to the HttpUtils helper class' do
event = Optimizely::Event.new(:post, @url, @params, @post_headers)
expect(Optimizely::Helpers::HttpUtils).to receive(:make_request).with(
event.url,
event.http_verb,
event.params.to_json,
event.headers,
Optimizely::EventDispatcher::REQUEST_TIMEOUT,
proxy_config
)

@customized_event_dispatcher.dispatch_event(event)
end
end

it 'should properly dispatch V2 (POST) events' do
stub_request(:post, @url)
event = Optimizely::Event.new(:post, @url, @params, @post_headers)
Expand Down
54 changes: 54 additions & 0 deletions spec/helpers/http_utils_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

# Copyright 2020, Optimizely and contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#
require 'spec_helper'
require 'optimizely/config/proxy_config'

describe Optimizely::Helpers::HttpUtils do
context 'passing in a proxy config' do
let(:url) { 'https://example.com' }
let(:http_method) { :get }
let(:host) { 'host' }
let(:port) { 1234 }
let(:username) { 'username' }
let(:password) { 'password' }
let(:http_class) { double(:http_class) }
let(:http) { double(:http) }

before do
allow(http_class).to receive(:new).and_return(http)
allow(http).to receive(:use_ssl=)
allow(http).to receive(:request)
end

context 'with a proxy config that inclues host, port, username, and password' do
let(:proxy_config) { Optimizely::ProxyConfig.new(host, port, username, password) }
it 'with a full proxy config, it proxies the web request' do
expect(Net::HTTP).to receive(:Proxy).with(host, port, username, password).and_return(http_class)
described_class.make_request(url, http_method, nil, nil, nil, proxy_config)
end
end

context 'with a proxy config that only inclues host' do
let(:proxy_config) { Optimizely::ProxyConfig.new(host) }
it 'with a full proxy config, it proxies the web request' do
expect(Net::HTTP).to receive(:Proxy).with(host, nil, nil, nil).and_return(http_class)
described_class.make_request(url, http_method, nil, nil, nil, proxy_config)
end
end
end
end