diff --git a/CHANGELOG.md b/CHANGELOG.md index c51b566..fbae90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 5.5.0 + - Feat: added `ssl_supported_protocols` option [#131](https://github.com/logstash-plugins/logstash-output-http/pull/131) + ## 5.4.1 - Fix retry indefinitely in termination process. This feature requires Logstash 8.1 [#129](https://github.com/logstash-plugins/logstash-output-http/pull/129) - Docs: Add retry policy description [#130](https://github.com/logstash-plugins/logstash-output-http/pull/130) diff --git a/docs/index.asciidoc b/docs/index.asciidoc index cf26708..b61bbc5 100644 --- a/docs/index.asciidoc +++ b/docs/index.asciidoc @@ -100,6 +100,7 @@ This plugin supports the following configuration options plus the <> |<>|No | <> |<>|No | <> |<>|No +| <> |<>|No | <> |<>|No | <> |a valid filesystem path|No | <> |<>|No @@ -378,6 +379,23 @@ See <> for more information. Timeout (in seconds) to wait for data on the socket. Default is `10s` +[id="plugins-{type}s-{plugin}-ssl_supported_protocols"] +===== `ssl_supported_protocols` + + * Value type is <> + * Allowed values are: `'TLSv1.1'`, `'TLSv1.2'`, `'TLSv1.3'` + * Default depends on the JDK being used. With up-to-date Logstash, the default is `['TLSv1.2', 'TLSv1.3']`. + `'TLSv1.1'` is not considered secure and is only provided for legacy applications. + +List of allowed SSL/TLS versions to use when establishing a connection to the HTTP endpoint. + +For Java 8 `'TLSv1.3'` is supported only since **8u262** (AdoptOpenJDK), but requires that you set the +`LS_JAVA_OPTS="-Djdk.tls.client.protocols=TLSv1.3"` system property in Logstash. + +NOTE: If you configure the plugin to use `'TLSv1.1'` on any recent JVM, such as the one packaged with Logstash, +the protocol is disabled by default and needs to be enabled manually by changing `jdk.tls.disabledAlgorithms` in +the *$JDK_HOME/conf/security/java.security* configuration file. That is, `TLSv1.1` needs to be removed from the list. + [id="plugins-{type}s-{plugin}-ssl_verification_mode"] ===== `ssl_verification_mode` diff --git a/lib/logstash/outputs/http.rb b/lib/logstash/outputs/http.rb index 63ef37a..3f9bac1 100644 --- a/lib/logstash/outputs/http.rb +++ b/lib/logstash/outputs/http.rb @@ -272,7 +272,7 @@ def send_event(event, attempt) :url => url, :method => @http_method, :message => exception.message, - :class => exception.class.name, + :class => exception.class, :will_retry => will_retry } if @logger.debug? diff --git a/logstash-output-http.gemspec b/logstash-output-http.gemspec index 4feeab5..56128f0 100644 --- a/logstash-output-http.gemspec +++ b/logstash-output-http.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = 'logstash-output-http' - s.version = '5.4.1' + s.version = '5.5.0' s.licenses = ['Apache License (2.0)'] s.summary = "Sends events to a generic HTTP or HTTPS endpoint" s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program" @@ -20,7 +20,7 @@ Gem::Specification.new do |s| # Gem dependencies s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99" - s.add_runtime_dependency "logstash-mixin-http_client", ">= 7.1.0", "< 8.0.0" + s.add_runtime_dependency "logstash-mixin-http_client", ">= 7.2.0", "< 8.0.0" s.add_development_dependency 'logstash-devutils' s.add_development_dependency 'sinatra' diff --git a/spec/outputs/http_spec.rb b/spec/outputs/http_spec.rb index e2e18d2..26d2f59 100644 --- a/spec/outputs/http_spec.rb +++ b/spec/outputs/http_spec.rb @@ -1,109 +1,4 @@ -require "logstash/devutils/rspec/spec_helper" -require "logstash/outputs/http" -require "logstash/codecs/plain" -require "thread" -require "sinatra" -require "webrick" -require "webrick/https" -require 'openssl' -require_relative "../supports/compressed_requests" - -PORT = rand(65535-1024) + 1025 - -class LogStash::Outputs::Http - attr_writer :agent - attr_reader :request_tokens -end - -# note that Sinatra startup and shutdown messages are directly logged to stderr so -# it is not really possible to disable them without reopening stderr which is not advisable. -# -# == Sinatra (v1.4.6) has taken the stage on 51572 for development with backup from WEBrick -# == Sinatra has ended his set (crowd applauds) -# -class TestApp < Sinatra::Base - # on the fly uncompress gzip content - use CompressedRequests - - set :environment, :production - set :sessions, false - - @@server_settings = { - :AccessLog => [], # disable WEBrick logging - :Logger => WEBrick::BasicLog::new(nil, WEBrick::BasicLog::FATAL) - } - - def self.server_settings - @@server_settings - end - - def self.server_settings=(settings) - @@server_settings = settings - end - - def self.multiroute(methods, path, &block) - methods.each do |method| - method.to_sym - self.send method, path, &block - end - end - - def self.last_request=(request) - @last_request = request - end - - def self.last_request - @last_request - end - - def self.retry_fail_count=(count) - @retry_fail_count = count - end - - def self.retry_fail_count() - @retry_fail_count || 2 - end - - multiroute(%w(get post put patch delete), "/good") do - self.class.last_request = request - [200, "YUP"] - end - - multiroute(%w(get post put patch delete), "/bad") do - self.class.last_request = request - [400, "YUP"] - end - - multiroute(%w(get post put patch delete), "/retry") do - self.class.last_request = request - - if self.class.retry_fail_count > 0 - self.class.retry_fail_count -= 1 - [429, "Will succeed in #{self.class.retry_fail_count}"] - else - [200, "Done Retrying"] - end - end -end - -RSpec.configure do - #http://stackoverflow.com/questions/6557079/start-and-call-ruby-http-server-in-the-same-script - def start_app_and_wait(app, opts = {}) - queue = Queue.new - - Thread.start do - begin - app.start!({ server: 'WEBrick', port: PORT }.merge opts) do |server| - queue.push(server) - end - rescue => e - warn "Error starting app: #{e.inspect}" # ignore - end - end - - queue.pop # blocks until the start! callback runs - end -end +require File.expand_path('../spec_helper.rb', File.dirname(__FILE__)) describe LogStash::Outputs::Http do # Wait for the async request to finish in this spinlock @@ -520,24 +415,28 @@ def start_app_and_wait(app, opts = {}) end end -describe LogStash::Outputs::Http do # different block as we're starting web server with TLS +RSpec.describe LogStash::Outputs::Http do # different block as we're starting web server with TLS @@default_server_settings = TestApp.server_settings.dup before do - cert, key = WEBrick::Utils.create_self_signed_cert 2048, [["CN", ssl_cert_host]], "Logstash testing" - TestApp.server_settings = @@default_server_settings.merge({ - :SSLEnable => true, - :SSLVerifyClient => OpenSSL::SSL::VERIFY_NONE, - :SSLCertificate => cert, - :SSLPrivateKey => key - }) + TestApp.server_settings = @@default_server_settings.merge(webrick_config) TestApp.last_request = nil @server = start_app_and_wait(TestApp) end + let(:webrick_config) do + cert, key = WEBrick::Utils.create_self_signed_cert 2048, [["CN", ssl_cert_host]], "Logstash testing" + { + SSLEnable: true, + SSLVerifyClient: OpenSSL::SSL::VERIFY_NONE, + SSLCertificate: cert, + SSLPrivateKey: key + } + end + after do @server.shutdown # WEBrick::HTTPServer @@ -590,4 +489,44 @@ def start_app_and_wait(app, opts = {}) end + context 'with supported_protocols set to (disabled) 1.1' do + + let(:config) { super().merge 'ssl_supported_protocols' => ['TLSv1.1'], 'ssl_verification_mode' => 'none' } + + it "keeps retrying due a protocol exception" do # TLSv1.1 not enabled by default + expect(subject).to receive(:log_failure). + with('Could not fetch URL', hash_including(message: 'No appropriate protocol (protocol is disabled or cipher suites are inappropriate)')). + at_least(:once) + Thread.start { subject.multi_receive [ event ] } + sleep 1.0 + end + + end unless tls_version_enabled_by_default?('TLSv1.1') + + context 'with supported_protocols set to 1.2/1.3' do + + let(:config) { super().merge 'ssl_supported_protocols' => ['TLSv1.2', 'TLSv1.3'], 'ssl_verification_mode' => 'none' } + + let(:webrick_config) { super().merge SSLVersion: 'TLSv1.2' } + + it "should process the request" do + subject.multi_receive [ event ] + expect(last_request_body).to include '"message":"hello!"' + end + + end + + context 'with supported_protocols set to 1.3' do + + let(:config) { super().merge 'ssl_supported_protocols' => ['TLSv1.3'], 'ssl_verification_mode' => 'none' } + + let(:webrick_config) { super().merge SSLVersion: 'TLSv1.3' } + + it "should process the request" do + subject.multi_receive [ event ] + expect(last_request_body).to include '"message":"hello!"' + end + + end if tls_version_enabled_by_default?('TLSv1.3') && JOpenSSL::VERSION > '0.12' # due WEBrick uses OpenSSL + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..3d8d563 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,136 @@ +require "logstash/devutils/rspec/spec_helper" +require "logstash/outputs/http" +require "logstash/codecs/plain" + +require "thread" +require "sinatra" +require "webrick" +require "webrick/https" +require 'openssl' + +require "supports/compressed_requests" + +PORT = rand(65535-1024) + 1025 + +class LogStash::Outputs::Http + attr_writer :agent + attr_reader :request_tokens +end + +# NOTE: extend WEBrick with support for config[:SSLVersion] +WEBrick::GenericServer.class_eval do + alias_method :__setup_ssl_context, :setup_ssl_context + + def setup_ssl_context(config) + ctx = __setup_ssl_context(config) + ctx.ssl_version = config[:SSLVersion] if config[:SSLVersion] + ctx + end + +end + +# note that Sinatra startup and shutdown messages are directly logged to stderr so +# it is not really possible to disable them without reopening stderr which is not advisable. +# +# == Sinatra (v1.4.6) has taken the stage on 51572 for development with backup from WEBrick +# == Sinatra has ended his set (crowd applauds) +# +class TestApp < Sinatra::Base + # on the fly uncompress gzip content + use CompressedRequests + + set :environment, :production + set :sessions, false + + @@server_settings = { + :AccessLog => [], # disable WEBrick logging + :Logger => WEBrick::BasicLog::new(nil, WEBrick::BasicLog::FATAL) + } + + def self.server_settings + @@server_settings + end + + def self.server_settings=(settings) + @@server_settings = settings + end + + def self.multiroute(methods, path, &block) + methods.each do |method| + method.to_sym + self.send method, path, &block + end + end + + def self.last_request=(request) + @last_request = request + end + + def self.last_request + @last_request + end + + def self.retry_fail_count=(count) + @retry_fail_count = count + end + + def self.retry_fail_count() + @retry_fail_count || 2 + end + + multiroute(%w(get post put patch delete), "/good") do + self.class.last_request = request + [200, "YUP"] + end + + multiroute(%w(get post put patch delete), "/bad") do + self.class.last_request = request + [400, "YUP"] + end + + multiroute(%w(get post put patch delete), "/retry") do + self.class.last_request = request + + if self.class.retry_fail_count > 0 + self.class.retry_fail_count -= 1 + [429, "Will succeed in #{self.class.retry_fail_count}"] + else + [200, "Done Retrying"] + end + end +end + +RSpec.configure do |config| + #http://stackoverflow.com/questions/6557079/start-and-call-ruby-http-server-in-the-same-script + def start_app_and_wait(app, opts = {}) + queue = Queue.new + + Thread.start do + begin + app.start!({ server: 'WEBrick', port: PORT }.merge opts) do |server| + yield(server) if block_given? + queue.push(server) + end + rescue => e + warn "Error starting app: #{e.inspect}" # ignore + end + end + + queue.pop # blocks until the start! callback runs + end + + config.extend(Module.new do + + def tls_version_enabled_by_default?(tls_version) + begin + context = javax.net.ssl.SSLContext.getInstance('TLS') + context.init nil, nil, nil + context.getDefaultSSLParameters.getProtocols.include? tls_version.to_s + rescue => e + warn "#{__method__} failed : #{e.inspect}" + nil + end + end + + end) +end \ No newline at end of file