Skip to content

Commit

Permalink
Rloomba/add proxy config (#262)
Browse files Browse the repository at this point in the history
* add ability to use http proxy when making web requests

* fix: linting

* ignore spellcheck

* headers and missing test

* update to use master mdspell

Co-authored-by: Ryan Loomba <ryan@loomba.io>
Co-authored-by: Owais Akbani <owais.akbani92@gmail.com>
  • Loading branch information
3 people authored Jul 8, 2020
1 parent 6ea2681 commit 5a18ac0
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 14 deletions.
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

0 comments on commit 5a18ac0

Please sign in to comment.