From c446869eceb16679a425079f3f4b94cdbd233860 Mon Sep 17 00:00:00 2001 From: saada Date: Thu, 14 Aug 2025 13:30:27 -0400 Subject: [PATCH 1/3] Add optional SSL verification override Add verify_ssl parameter (defaults to true) to allow disabling SSL certificate verification for development, testing, and internal networks. This maintains security by default while providing flexibility when needed. --- lib/ld-eventsource/client.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/ld-eventsource/client.rb b/lib/ld-eventsource/client.rb index 2e388fe..a580e98 100644 --- a/lib/ld-eventsource/client.rb +++ b/lib/ld-eventsource/client.rb @@ -6,6 +6,7 @@ require "concurrent/atomics" require "logger" +require "openssl" require "thread" require "uri" require "http" @@ -91,6 +92,7 @@ class Client # request to generate the payload dynamically. # @param retry_enabled [Boolean] (true) whether to retry connections after failures. If false, the client # will exit after the first connection failure instead of attempting to reconnect. + # @param verify_ssl [Boolean] (true) whether to verify SSL certificates; set to false for development/testing # @yieldparam [Client] client the new client instance, before opening the connection # def initialize(uri, @@ -105,7 +107,8 @@ def initialize(uri, socket_factory: nil, method: "GET", payload: nil, - retry_enabled: true) + retry_enabled: true, + verify_ssl: true) @uri = URI(uri) @stopped = Concurrent::AtomicBoolean.new(false) @retry_enabled = retry_enabled @@ -117,6 +120,9 @@ def initialize(uri, @payload = payload @logger = logger || default_logger http_client_options = {} + unless verify_ssl + http_client_options[:ssl] = { verify_mode: OpenSSL::SSL::VERIFY_NONE } + end if socket_factory http_client_options["socket_class"] = socket_factory end From f48a756ee8b82a8f30fcd42f48fe09f7b04abc06 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 20 Aug 2025 16:47:18 -0400 Subject: [PATCH 2/3] Replace verify_ssl with more generic http_client_options --- lib/ld-eventsource/client.rb | 21 ++--- spec/client_spec.rb | 149 +++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 10 deletions(-) diff --git a/lib/ld-eventsource/client.rb b/lib/ld-eventsource/client.rb index a580e98..f2eed91 100644 --- a/lib/ld-eventsource/client.rb +++ b/lib/ld-eventsource/client.rb @@ -6,7 +6,6 @@ require "concurrent/atomics" require "logger" -require "openssl" require "thread" require "uri" require "http" @@ -92,7 +91,9 @@ class Client # request to generate the payload dynamically. # @param retry_enabled [Boolean] (true) whether to retry connections after failures. If false, the client # will exit after the first connection failure instead of attempting to reconnect. - # @param verify_ssl [Boolean] (true) whether to verify SSL certificates; set to false for development/testing + # @param http_client_options [Hash] (nil) additional options to pass to + # the HTTP client, such as `socket_factory` or `proxy`. These settings will override + # the socket factory and proxy settings. # @yieldparam [Client] client the new client instance, before opening the connection # def initialize(uri, @@ -108,7 +109,7 @@ def initialize(uri, method: "GET", payload: nil, retry_enabled: true, - verify_ssl: true) + http_client_options: nil) @uri = URI(uri) @stopped = Concurrent::AtomicBoolean.new(false) @retry_enabled = retry_enabled @@ -119,12 +120,10 @@ def initialize(uri, @method = method.to_s.upcase @payload = payload @logger = logger || default_logger - http_client_options = {} - unless verify_ssl - http_client_options[:ssl] = { verify_mode: OpenSSL::SSL::VERIFY_NONE } - end + + base_http_client_options = {} if socket_factory - http_client_options["socket_class"] = socket_factory + base_http_client_options["socket_class"] = socket_factory end if proxy @@ -137,13 +136,15 @@ def initialize(uri, end if @proxy - http_client_options["proxy"] = { + base_http_client_options["proxy"] = { :proxy_address => @proxy.host, :proxy_port => @proxy.port, } end - @http_client = HTTP::Client.new(http_client_options) + options = http_client_options.is_a?(Hash) ? base_http_client_options.merge(http_client_options) : base_http_client_options + + @http_client = HTTP::Client.new(options) .follow .timeout({ read: read_timeout, diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 12fde0d..4817b87 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -738,6 +738,155 @@ def test_object.to_s end end + describe "http_client_options precedence" do + it "allows socket_factory to be set via individual parameter" do + mock_socket_factory = double("MockSocketFactory") + + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, "", keep_open: true) + end + + # We can't easily test socket creation without actually making a connection, + # but we can verify the options contain the socket_class + client = nil + expect { + client = subject.new(server.base_uri, socket_factory: mock_socket_factory) + }.not_to raise_error + + # Access the internal HTTP client to verify socket_class was set + expect(client.instance_variable_get(:@http_client).default_options.socket_class).to eq(mock_socket_factory) + + client.close + end + end + + it "allows proxy to be set via individual parameter" do + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, simple_event_1_text, keep_open: false) + end + + with_server(StubProxyServer.new) do |proxy| + event_sink = Queue.new + client = subject.new(server.base_uri, proxy: proxy.base_uri) do |c| + c.on_event { |event| event_sink << event } + end + + with_client(client) do |c| + expect(event_sink.pop).to eq(simple_event_1) + expect(proxy.request_count).to eq(1) + end + end + end + end + + it "allows http_client_options to override socket_factory" do + individual_socket_factory = double("IndividualSocketFactory") + override_socket_factory = double("OverrideSocketFactory") + + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, "", keep_open: true) + end + + # http_client_options should take precedence over individual parameter + client = nil + expect { + client = subject.new(server.base_uri, + socket_factory: individual_socket_factory, + http_client_options: {"socket_class" => override_socket_factory}) + }.not_to raise_error + + # Verify that the override socket factory was used, not the individual one + expect(client.instance_variable_get(:@http_client).default_options.socket_class).to eq(override_socket_factory) + + client.close + end + end + + it "allows http_client_options to override proxy settings" do + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, simple_event_1_text, keep_open: false) + end + + with_server(StubProxyServer.new) do |individual_proxy| + with_server(StubProxyServer.new) do |override_proxy| + event_sink = Queue.new + client = subject.new(server.base_uri, + proxy: individual_proxy.base_uri, + http_client_options: {"proxy" => { + :proxy_address => override_proxy.base_uri.host, + :proxy_port => override_proxy.base_uri.port + }}) do |c| + c.on_event { |event| event_sink << event } + end + + with_client(client) do |c| + expect(event_sink.pop).to eq(simple_event_1) + # The override proxy should be used, not the individual one + expect(override_proxy.request_count).to eq(1) + expect(individual_proxy.request_count).to eq(0) + end + end + end + end + end + + it "merges http_client_options with base options when both socket_factory and other options are provided" do + socket_factory = double("SocketFactory") + ssl_options = { verify_mode: 0 } # OpenSSL::SSL::VERIFY_NONE equivalent + + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, "", keep_open: true) + end + + # Should include both socket_factory from individual param and ssl from http_client_options + client = nil + expect { + client = subject.new(server.base_uri, + socket_factory: socket_factory, + http_client_options: {"ssl" => ssl_options}) + }.not_to raise_error + + # Verify both options are present + http_options = client.instance_variable_get(:@http_client).default_options + expect(http_options.socket_class).to eq(socket_factory) + expect(http_options.ssl).to eq(ssl_options) + + client.close + end + end + end + + describe "http_client_options SSL pass-through" do + it "passes SSL verification options through http_client_options" do + ssl_options = { + verify_mode: 0, # OpenSSL::SSL::VERIFY_NONE equivalent + verify_hostname: false, + } + + with_server do |server| + server.setup_response("/") do |req,res| + send_stream_content(res, "", keep_open: true) + end + + client = nil + expect { + client = subject.new(server.base_uri, + http_client_options: {"ssl" => ssl_options}) + }.not_to raise_error + + # Verify SSL options are passed through + expect(client.instance_variable_get(:@http_client).default_options.ssl).to eq(ssl_options) + + client.close + end + end + end + describe "retry parameter" do it "defaults to true (retries enabled)" do events_body = simple_event_1_text From c7706b0ead13d174839b3cb1bcfa1e5927da6a28 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 21 Aug 2025 09:30:53 -0400 Subject: [PATCH 3/3] rubocop -a --- spec/client_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 4817b87..62c4968 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -818,7 +818,7 @@ def test_object.to_s proxy: individual_proxy.base_uri, http_client_options: {"proxy" => { :proxy_address => override_proxy.base_uri.host, - :proxy_port => override_proxy.base_uri.port + :proxy_port => override_proxy.base_uri.port, }}) do |c| c.on_event { |event| event_sink << event } end