diff --git a/fluentd.gemspec b/fluentd.gemspec index 94551a2af9..a97e242e44 100644 --- a/fluentd.gemspec +++ b/fluentd.gemspec @@ -49,4 +49,5 @@ Gem::Specification.new do |gem| gem.add_development_dependency("test-unit-rr", ["~> 1.0"]) gem.add_development_dependency("oj", [">= 2.14", "< 4"]) gem.add_development_dependency("ext_monitor", [">= 0.1.1", "< 0.2"]) + gem.add_development_dependency("async-http") end diff --git a/test/plugin_helper/test_http_server_helper.rb b/test/plugin_helper/test_http_server_helper.rb index 292734a522..fc15b14300 100644 --- a/test/plugin_helper/test_http_server_helper.rb +++ b/test/plugin_helper/test_http_server_helper.rb @@ -5,18 +5,25 @@ require 'fluent/event' require 'net/http' require 'uri' +require 'openssl' +require 'async' class HtttpHelperTest < Test::Unit::TestCase PORT = unused_port NULL_LOGGER = Logger.new(nil) + TMP_DIR = File.expand_path(File.dirname(__FILE__) + '/../tmp/plugin_helper_http_server') + CERT_DIR = File.expand_path(File.dirname(__FILE__) + '/data/cert/without_ca') + CERT_CA_DIR = File.expand_path(File.dirname(__FILE__) + '/data/cert/with_ca') class Dummy < Fluent::Plugin::TestBase helpers :http_server end - def on_driver + def on_driver(config = nil) + config ||= Fluent::Config.parse(config || '', '(name)', '') Fluent::Test.setup driver = Dummy.new + driver.configure(config) driver.start driver.after_start @@ -47,6 +54,12 @@ def on_driver end end + def on_driver_transport(opts = {}, &block) + transport_conf = config_element('transport', 'tls', opts) + c = config_element('ROOT', '', {}, [transport_conf]) + on_driver(c, &block) + end + %w[get head].each do |n| define_method(n) do |uri, header = {}| url = URI.parse(uri) @@ -56,6 +69,14 @@ def on_driver http.request(req) end end + + define_method("secure_#{n}") do |uri, header = {}, verify: true, cert_path: nil, selfsigned: true, hostname: false| + url = URI.parse(uri) + headers = { 'Content-Type' => 'application/x-www-form-urlencoded/' }.merge(header) + start_https_request(url.host, url.port, verify: verify, cert_path: cert_path, selfsigned: selfsigned) do |https| + https.send(n, url.path, headers.to_a) + end + end end %w[post put patch delete options trace].each do |n| @@ -70,6 +91,91 @@ def on_driver end end + # wrapper for net/http + Response = Struct.new(:code, :body, :headers) + + # Use async-http as http client since net/http can't be set verify_hostname= now + # will be replaced when net/http supports verify_hostname= + def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: true, hostname: nil) + context = OpenSSL::SSL::SSLContext.new + context.set_params({}) + if verify + cert_store = OpenSSL::X509::Store.new + cert_store.set_default_paths + if selfsigned && OpenSSL::X509.const_defined?('V_FLAG_CHECK_SS_SIGNATURE') + cert_store.flags = OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE + end + + if cert_path + cert_store.add_file(cert_path) + end + + context.cert_store = cert_store + if !hostname && context.respond_to?(:verify_hostname=) + context.verify_hostname = false # In test code, using hostname to be connected is very difficult + end + + context.verify_mode = OpenSSL::SSL::VERIFY_PEER + else + context.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + + client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse("https://#{addr}:#{port}", ssl_context: context)) + reactor = Async::Reactor.new(nil, logger: NULL_LOGGER) + + resp = nil + error = nil + + reactor.run do + begin + response = yield(client) + rescue => e + error = e + end + + resp = Response.new(response.status.to_s, response.body.read, response.headers) + end + + if error + raise error + else + resp + end + end + + # def start_https_request(addr, port, verify: true, cert_path: nil, selfsigned: true) + # https = Net::HTTP.new(addr, port) + # https.use_ssl = true + + # if verify + # cert_store = OpenSSL::X509::Store.new + # cert_store.set_default_paths + # if selfsigned && OpenSSL::X509.const_defined?('V_FLAG_CHECK_SS_SIGNATURE') + # cert_store.flags = OpenSSL::X509::V_FLAG_CHECK_SS_SIGNATURE + # end + + # if cert_path + # cert_store.add_file(cert_path) + # end + + # https.cert_store = cert_store + + # # https.verify_hostname = false + + # https.verify_mode = OpenSSL::SSL::VERIFY_PEER + # else + # https.verify_mode = OpenSSL::SSL::VERIFY_NONE + # end + + # # if !hostname && context.respond_to?(:verify_hostname=) + # # context.verify_hostname = false # In test code, using hostname to be connected is very difficult + # # end + + # https.start do + # yield(https) + # end + # end + sub_test_case 'Create a HTTP server' do test 'monunt given path' do on_driver do |driver| @@ -185,6 +291,95 @@ def on_driver end end + sub_test_case 'create a HTTPS server' do + test '#configure' do + driver = Dummy.new + + transport_conf = config_element('transport', 'tls', { 'version' => 'TLSv1_1' }) + driver.configure(config_element('ROOT', '', {}, [transport_conf])) + assert_equal :tls, driver.transport_config.protocol + assert_equal :TLSv1_1, driver.transport_config.version + end + + sub_test_case '#http_server_create_https_server' do + test 'can overwrite settings by using tls_context' do + on_driver_transport({ 'insecure' => 'false' }) do |driver| + tls = { 'insecure' => 'true' } # overwrite + driver.http_server_create_https_server(:http_server_helper_test_tls, addr: '127.0.0.1', port: PORT, logger: NULL_LOGGER, tls_opts: tls) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello get'] } + end + + resp = secure_get("https://127.0.0.1:#{PORT}/example/hello", verify: false) + assert_equal('200', resp.code) + assert_equal('hello get', resp.body) + end + end + + test 'with insecure in transport section' do + on_driver_transport({ 'insecure' => 'true' }) do |driver| + driver.http_server_create_https_server(:http_server_helper_test_tls, addr: '127.0.0.1', port: PORT, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello get'] } + end + + resp = secure_get("https://127.0.0.1:#{PORT}/example/hello", verify: false) + assert_equal('200', resp.code) + assert_equal('hello get', resp.body) + + assert_raise OpenSSL::SSL::SSLError do + secure_get("https://127.0.0.1:#{PORT}/example/hello") + end + end + end + + data( + 'with passphrase' => ['apple', 'cert-pass.pem', 'cert-key-pass.pem'], + 'without passphrase' => [nil, 'cert.pem', 'cert-key.pem']) + test 'load self-signed cert/key pair, verified from clients using cert files' do |(passphrase, cert, private_key)| + cert_path = File.join(CERT_DIR, cert) + private_key_path = File.join(CERT_DIR, private_key) + opt = { 'insecure' => 'false', 'private_key_path' => private_key_path, 'cert_path' => cert_path } + if passphrase + opt['private_key_passphrase'] = passphrase + end + + on_driver_transport(opt) do |driver| + driver.http_server_create_https_server(:http_server_helper_test_tls, addr: '127.0.0.1', port: PORT, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello get'] } + end + + resp = secure_get("https://127.0.0.1:#{PORT}/example/hello", cert_path: cert_path) + assert_equal('200', resp.code) + assert_equal('hello get', resp.body) + end + end + + data( + 'with passphrase' => ['apple', 'cert-pass.pem', 'cert-key-pass.pem', 'ca-cert-pass.pem'], + 'without passphrase' => [nil, 'cert.pem', 'cert-key.pem', 'ca-cert.pem']) + test 'load cert by private CA cert file, verified from clients using CA cert file' do |(passphrase, cert, cert_key, ca_cert)| + cert_path = File.join(CERT_CA_DIR, cert) + private_key_path = File.join(CERT_CA_DIR, cert_key) + + ca_cert_path = File.join(CERT_CA_DIR, ca_cert) + + opt = { 'insecure' => 'false', 'cert_path' => cert_path, 'private_key_path' => private_key_path } + if passphrase + opt['private_key_passphrase'] = passphrase + end + + on_driver_transport(opt) do |driver| + driver.http_server_create_https_server(:http_server_helper_test_tls, addr: '127.0.0.1', port: PORT, logger: NULL_LOGGER) do |s| + s.get('/example/hello') { [200, { 'Content-Type' => 'text/plain' }, 'hello get'] } + end + + resp = secure_get("https://127.0.0.1:#{PORT}/example/hello", cert_path: ca_cert_path) + assert_equal('200', resp.code) + assert_equal('hello get', resp.body) + end + end + end + end + test 'must be called #start and #stop' do on_driver do |driver| server = flexmock('Server') do |watcher|