diff --git a/CHANGELOG.md b/CHANGELOG.md index 9887cd190e..56b7ed674e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # v1.19 +## Release v1.19.1 - TBD + +### Bug fix + +* http_server helper: Fix IPv6 bind address support in URI construction + * Fixed `URI::InvalidURIError` when binding to IPv6 addresses (e.g., `::`, `::1`) + * IPv6 addresses are now properly bracketed in URIs per RFC 3986 (e.g., `http://[::]:24231`) + * Handles pre-bracketed addresses correctly to avoid double-bracketing + * Affects all plugins using http_server helper with IPv6 bind addresses + ## Release v1.19.0 - 2025/07/30 ### Enhancement diff --git a/lib/fluent/plugin_helper/http_server/server.rb b/lib/fluent/plugin_helper/http_server/server.rb index 9c099292d8..b59450c242 100644 --- a/lib/fluent/plugin_helper/http_server/server.rb +++ b/lib/fluent/plugin_helper/http_server/server.rb @@ -37,7 +37,16 @@ def initialize(addr:, port:, logger:, default_app: nil, tls_context: nil) # TODO: support http2 scheme = tls_context ? 'https' : 'http' - @uri = URI("#{scheme}://#{@addr}:#{@port}").to_s + # Handle IPv6 addresses properly in URI construction per RFC 3986 + # Check if address is already bracketed to avoid double-bracketing + addr_display = if @addr.include?(":") && !@addr.start_with?('[') + "[#{@addr}]" # Bare IPv6 address - add brackets + elsif @addr.start_with?('[') && @addr.end_with?(']') + @addr # Already bracketed - use as-is + else + @addr # IPv4 or hostname - use directly + end + @uri = URI("#{scheme}://#{addr_display}:#{@port}").to_s @router = Router.new(default_app) @server_task = nil Console.logger = Fluent::Log::ConsoleAdapter.wrap(@logger) diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index bc9853c013..f71400ded3 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -21,6 +21,22 @@ def teardown @port = nil end + def ipv6_enabled? + begin + # Try to actually bind to an IPv6 address to verify it works + sock = Socket.new(Socket::AF_INET6, Socket::SOCK_STREAM, 0) + sock.bind(Socket.sockaddr_in(0, '::1')) + sock.close + + # Also test that we can resolve IPv6 addresses + # This is needed because some systems can bind but can't connect + Socket.getaddrinfo('::1', nil, Socket::AF_INET6) + true + rescue Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, SocketError + false + end + end + class Dummy < Fluent::Plugin::TestBase helpers :http_server end @@ -364,6 +380,57 @@ def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: tr end end + test 'bind to IPv6 address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '::1', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6'] } + end + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from ipv6', resp.body) + end + end + + test 'bind to IPv6 wildcard address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '::', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from ipv6 wildcard'] } + end + # Can access via IPv4-mapped IPv6 or IPv6 loopback + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from ipv6 wildcard', resp.body) + end + end + + test 'handle already bracketed IPv6 address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + # Test that pre-bracketed addresses don't get double-bracketed + driver.http_server_create_http_server(:http_server_helper_test, addr: '[::1]', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from bracketed ipv6'] } + end + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from bracketed ipv6', resp.body) + end + end + + test 'handle already bracketed IPv6 wildcard address' do + omit('IPv6 not supported') unless ipv6_enabled? + on_driver do |driver| + driver.http_server_create_http_server(:http_server_helper_test, addr: '[::]', port: @port, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello from bracketed ipv6 wildcard'] } + end + # Access via IPv6 loopback + resp = get("http://[::1]:#{@port}/example/hello") + assert_equal('200', resp.code) + assert_equal('hello from bracketed ipv6 wildcard', resp.body) + end + end + test 'must be called #start and #stop' do on_driver do |driver| server = flexmock('Server') do |watcher|