Skip to content

Commit c2f8ada

Browse files
authored
Add HTTP proxy support
- Add proxy option to Twingly::HTTP::Client initialization - Configure Faraday to route requests through specified proxy if provided
1 parent ad52c2b commit c2f8ada

9 files changed

Lines changed: 157 additions & 1 deletion

File tree

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ gemspec
66

77
group :development, :test do
88
gem "climate_control", "~> 0.1"
9+
gem "rack", "~> 3.1"
10+
gem "rack-proxy", "~> 0.7.7"
11+
gem "rackup", "~> 2.2"
912
gem "rake", "~> 12"
1013
gem "rspec", "~> 3"
1114
gem "rubocop", "~> 1.64.1"
1215
gem "rubocop-rspec", "~> 2.31.0"
1316
gem "toxiproxy", "~> 1.0"
1417
gem "vcr", "~> 5.0"
1518
gem "webmock", "~> 3.7"
19+
gem "webrick", "~> 1.9"
1620
end

lib/twingly/http.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ class Client # rubocop:disable Metrics/ClassLength
5050
attr_accessor :logger
5151
attr_accessor :retryable_exceptions
5252

53-
def initialize(base_user_agent:, logger: default_logger, user_agent: nil)
53+
def initialize(base_user_agent:, logger: default_logger, user_agent: nil, proxy: nil)
5454
@base_user_agent = base_user_agent
5555
@logger = logger
5656
@user_agent = user_agent
57+
@proxy = proxy
5758

5859
initialize_defaults
5960
end
@@ -212,6 +213,7 @@ def create_http_client # rubocop:disable Metrics/MethodLength
212213
max_size_bytes: @max_response_body_size_bytes
213214
faraday.adapter Faraday.default_adapter
214215
faraday.headers[:user_agent] = user_agent
216+
faraday.proxy = @proxy if @proxy
215217
end
216218
end
217219

spec/lib/twingly/http_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative "../../spec_help/http_test_server"
4+
35
class CustomError < StandardError; end
46

57
# rubocop:disable RSpec/MultipleMemoizedHelpers
@@ -533,6 +535,31 @@ class CustomError < StandardError; end
533535
end
534536
end
535537

538+
RSpec.shared_examples "verifies proxy functionality" do
539+
context "when a proxy is provided" do
540+
let(:proxy_server) { HttpTestServer.spawn("proxy_server") }
541+
let(:target_server) { HttpTestServer.spawn("echoed_headers_in_body") }
542+
let(:client) do
543+
described_class.new(
544+
base_user_agent: base_user_agent,
545+
proxy: proxy_server.url
546+
)
547+
end
548+
let(:url) { target_server.url }
549+
550+
after do
551+
HttpTestServer.stop(proxy_server.pid)
552+
HttpTestServer.stop(target_server.pid)
553+
end
554+
555+
it "routes requests through the proxy", vcr: false do
556+
with_real_http_connections do
557+
expect(response.fetch(:body)).to include("HTTP_X_PROXIED_BY")
558+
end
559+
end
560+
end
561+
end
562+
536563
describe "#initialize" do
537564
context "when no logger is given" do
538565
subject(:default_logger) do
@@ -552,6 +579,7 @@ class CustomError < StandardError; end
552579

553580
describe "#post", vcr: Fixture.post_example_org do
554581
include_examples "common HTTP behaviour for", :post, "example.org"
582+
include_examples "verifies proxy functionality"
555583

556584
let(:post_body) { nil }
557585
let(:post_headers) { {} }
@@ -600,6 +628,7 @@ class CustomError < StandardError; end
600628

601629
describe "#get", vcr: Fixture.example_org do
602630
include_examples "common HTTP behaviour for", :get, "example.org"
631+
include_examples "verifies proxy functionality"
603632

604633
let(:request_response) do
605634
client.get(url)
@@ -692,6 +721,7 @@ class CustomError < StandardError; end
692721

693722
describe "#put", vcr: Fixture.put_httpbin_org do
694723
include_examples "common HTTP behaviour for", :put, "https://httpbin.org/put"
724+
include_examples "verifies proxy functionality"
695725

696726
let(:url) { "https://httpbin.org/put" }
697727

@@ -742,6 +772,7 @@ class CustomError < StandardError; end
742772

743773
describe "#patch", vcr: Fixture.patch_httpbin_org do
744774
include_examples "common HTTP behaviour for", :patch, "https://httpbin.org/patch"
775+
include_examples "verifies proxy functionality"
745776

746777
let(:url) { "https://httpbin.org/patch" }
747778

@@ -792,6 +823,7 @@ class CustomError < StandardError; end
792823

793824
describe "#delete", vcr: Fixture.delete_httpbin_org do
794825
include_examples "common HTTP behaviour for", :delete, "https://httpbin.org/delete"
826+
include_examples "verifies proxy functionality"
795827

796828
let(:url) { "https://httpbin.org/delete" }
797829

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
5+
run lambda { |env|
6+
request_headers = env.select { |k, _v| k.start_with? "HTTP_" }
7+
8+
[200, { "content-type" => "application/json" }, [request_headers.to_json]]
9+
}

spec/rack_servers/proxy_server.ru

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
require "rack/proxy"
4+
5+
class TestProxy < Rack::Proxy
6+
def rewrite_env(env)
7+
env["HTTP_X_PROXIED_BY"] = "test-proxy"
8+
env
9+
end
10+
end
11+
12+
run TestProxy.new

spec/spec_help/http_helpers.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
module HttpHelpers
4+
def with_real_http_connections
5+
original_cassette = VCR.eject_cassette
6+
VCR.turn_off!
7+
WebMock.allow_net_connect!
8+
9+
yield
10+
ensure
11+
WebMock.disable_net_connect!
12+
VCR.turn_on!
13+
VCR.insert_cassette(original_cassette.name) if original_cassette
14+
end
15+
end

spec/spec_help/http_test_server.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
require "timeout"
4+
5+
module HttpTestServer
6+
module_function
7+
8+
TestServer = Struct.new(:pid, :url)
9+
10+
def spawn(server_name, env: {}) # rubocop:disable Metrics/MethodLength
11+
ip_address = PortProber.localhost
12+
port = PortProber.random(ip_address)
13+
url = "http://#{ip_address}:#{port}"
14+
server = "spec/rack_servers/#{server_name}.ru"
15+
command = "bundle exec rackup --quiet --port #{port} #{server}"
16+
17+
puts "starting HTTP test server: #{command}"
18+
pid = fork do
19+
$stdout.reopen File::NULL
20+
$stderr.reopen File::NULL
21+
exec env, command
22+
end
23+
24+
Timeout.timeout(10.0) do
25+
sleep 0.05 until started?(pid) && PortProber.port_open?(ip_address, port)
26+
end
27+
28+
TestServer.new(pid, url)
29+
end
30+
31+
def stop(pid)
32+
Process.kill(:TERM, pid)
33+
Process.wait(pid)
34+
end
35+
36+
def started?(pid)
37+
Process.getpgid(pid)
38+
true
39+
rescue Errno::ESRCH
40+
false
41+
end
42+
end

spec/spec_help/port_prober.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
require "socket"
4+
require "timeout"
5+
6+
module PortProber
7+
module_function
8+
9+
def random(host)
10+
server = TCPServer.new(host, 0)
11+
port = server.addr[1]
12+
13+
port
14+
ensure
15+
server&.close
16+
end
17+
18+
def port_open?(ip_address, port)
19+
Timeout.timeout(0.5) do
20+
TCPSocket.new(ip_address, port).close
21+
true
22+
end
23+
rescue StandardError
24+
false
25+
end
26+
27+
def localhost
28+
info = Socket.getaddrinfo("localhost",
29+
80,
30+
Socket::AF_INET,
31+
Socket::SOCK_STREAM)
32+
33+
raise "unable to translate 'localhost' for TCP + IPv4" if info.empty?
34+
35+
info[0][3]
36+
end
37+
end

spec/spec_helper.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
require_relative "spec_help/env_helper"
1616
require_relative "spec_help/fixture"
17+
require_relative "spec_help/http_helpers"
1718
require_relative "spec_help/test_logger"
1819
require_relative "spec_help/toxiproxy_config"
20+
require_relative "spec_help/port_prober"
1921

2022
# Start with a clean slate, destroy all proxies if any
2123
Toxiproxy.all.destroy
@@ -33,6 +35,7 @@
3335

3436
RSpec.configure do |conf|
3537
conf.include EnvHelper
38+
conf.include HttpHelpers
3639

3740
conf.after(:suite) do
3841
Toxiproxy.all.destroy # Be nice, end with a clean slate

0 commit comments

Comments
 (0)