fluent-plugin-d 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2021 D.Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require_relative 'lookup_from_const'
17
+
18
+ module SyslogTls
19
+ module Severity
20
+ extend LookupFromConst
21
+ EMERG = PANIC = 0
22
+ ALERT = 1
23
+ CRIT = 2
24
+ ERR = ERROR = 3
25
+ WARN = WARNING = 4
26
+ NOTICE = 5
27
+ INFO = 6
28
+ DEBUG = 7
29
+ NONE = 10
30
+ end
31
+ end
@@ -0,0 +1,190 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2016-2019 t.e.morgan.
3
+ # Copyright 2021 D.Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'socket'
18
+ require 'openssl'
19
+
20
+ module SyslogTls
21
+ # Supports SSL connection to remote host
22
+ class SSLTransport
23
+ CONNECT_TIMEOUT = 10
24
+ # READ_TIMEOUT = 5
25
+ WRITE_TIMEOUT = 5
26
+
27
+ attr_accessor :socket
28
+
29
+ attr_reader :host, :port, :idle_timeout, :ca_cert, :client_cert, :client_key, :verify_cert_name, :ssl_version
30
+
31
+ attr_writer :retries
32
+
33
+ def initialize(host, port, idle_timeout: nil, ca_cert: 'system', client_cert: nil, client_key: nil, verify_cert_name: true, ssl_version: :TLSv1_2, max_retries: 1)
34
+ @host = host
35
+ @port = port
36
+ @idle_timeout = idle_timeout
37
+ @ca_cert = ca_cert
38
+ @client_cert = client_cert
39
+ @client_key = client_key
40
+ @verify_cert_name = verify_cert_name
41
+ @ssl_version = ssl_version
42
+ @retries = max_retries
43
+ connect
44
+ end
45
+
46
+ def connect
47
+ @socket = get_ssl_connection
48
+ begin
49
+ begin
50
+ @socket.connect_nonblock
51
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
52
+ select_with_timeout(@socket, :connect_read) && retry
53
+ rescue IO::WaitWritable
54
+ select_with_timeout(@socket, :connect_write) && retry
55
+ end
56
+ rescue Errno::ETIMEDOUT
57
+ raise 'Socket timeout during connect'
58
+ end
59
+ @last_write = Time.now if idle_timeout
60
+ end
61
+
62
+ def get_tcp_connection
63
+ tcp = nil
64
+
65
+ family = Socket::Constants::AF_UNSPEC
66
+ sock_type = Socket::Constants::SOCK_STREAM
67
+ addr_info = Socket.getaddrinfo(host, port, family, sock_type, nil, nil, false).first
68
+ _, port, _, address, family, sock_type = addr_info
69
+
70
+ begin
71
+ sock_addr = Socket.sockaddr_in(port, address)
72
+ tcp = Socket.new(family, sock_type, 0)
73
+ tcp.setsockopt(Socket::SOL_SOCKET, Socket::Constants::SO_REUSEADDR, true)
74
+ tcp.setsockopt(Socket::SOL_SOCKET, Socket::Constants::SO_REUSEPORT, true)
75
+ tcp.connect_nonblock(sock_addr)
76
+ rescue Errno::EINPROGRESS
77
+ select_with_timeout(tcp, :connect_write)
78
+ begin
79
+ tcp.connect_nonblock(sock_addr)
80
+ rescue Errno::EISCONN
81
+ # all good
82
+ rescue SystemCallError
83
+ tcp.close rescue nil
84
+ raise
85
+ end
86
+ rescue SystemCallError
87
+ tcp.close rescue nil
88
+ raise
89
+ end
90
+
91
+ tcp.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
92
+ tcp
93
+ end
94
+
95
+ def get_ssl_connection
96
+ tcp = get_tcp_connection
97
+
98
+ ctx = OpenSSL::SSL::SSLContext.new
99
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
100
+ ctx.ssl_version = ssl_version
101
+
102
+ ctx.verify_hostname = verify_cert_name != false
103
+
104
+ case ca_cert
105
+ when true, 'true', 'system'
106
+ # use system certs, same as openssl cli
107
+ ctx.cert_store = OpenSSL::X509::Store.new
108
+ ctx.cert_store.set_default_paths
109
+ when false, 'false'
110
+ ctx.verify_hostname = false
111
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
112
+ when %r{/$} # ends in /
113
+ ctx.ca_path = ca_cert
114
+ when String
115
+ ctx.ca_file = ca_cert
116
+ end
117
+
118
+ ctx.cert = OpenSSL::X509::Certificate.new(File.read(client_cert)) if client_cert
119
+ ctx.key = OpenSSL::PKey::read(File.read(client_key)) if client_key
120
+ socket = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
121
+ socket.hostname = host
122
+ socket.sync_close = true
123
+ socket
124
+ end
125
+
126
+ # Allow to retry on failed writes
127
+ def write(s)
128
+ if idle_timeout
129
+ if (t=Time.now) > @last_write + idle_timeout
130
+ @socket.close rescue nil
131
+ connect
132
+ else
133
+ @last_write = t
134
+ end
135
+ end
136
+ begin
137
+ retry_id ||= 0
138
+ do_write(s)
139
+ rescue => e
140
+ if (retry_id += 1) < @retries
141
+ @socket.close rescue nil
142
+ connect
143
+ retry
144
+ else
145
+ raise e
146
+ end
147
+ end
148
+ end
149
+
150
+ def do_write(data)
151
+ data.force_encoding('BINARY') # so we can break in the middle of multi-byte characters
152
+ loop do
153
+ sent = 0
154
+ begin
155
+ sent = @socket.write_nonblock(data)
156
+ rescue OpenSSL::SSL::SSLError, Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable => e
157
+ if e.is_a?(OpenSSL::SSL::SSLError) && e.message !~ /write would block/
158
+ raise e
159
+ else
160
+ select_with_timeout(@socket, :write) && retry
161
+ end
162
+ end
163
+
164
+ break if sent >= data.size
165
+ data = data[sent, data.size]
166
+ end
167
+ end
168
+
169
+ def select_with_timeout(tcp, type)
170
+ case type
171
+ when :connect_read
172
+ args = [[tcp], nil, nil, CONNECT_TIMEOUT]
173
+ when :connect_write
174
+ args = [nil, [tcp], nil, CONNECT_TIMEOUT]
175
+ # when :read
176
+ # args = [[tcp], nil, nil, READ_TIMEOUT]
177
+ when :write
178
+ args = [nil, [tcp], nil, WRITE_TIMEOUT]
179
+ else
180
+ raise "Unknown select type #{type}"
181
+ end
182
+ IO.select(*args) || raise("Socket timeout during #{type}")
183
+ end
184
+
185
+ # Forward any methods directly to SSLSocket
186
+ def method_missing(method_sym, *arguments, &block)
187
+ @socket.send(method_sym, *arguments, &block)
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,19 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2016-2019 t.e.morgan.
3
+ # Copyright 2021 D.Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module SyslogTls
18
+ VERSION = '0.0.1'
19
+ end
@@ -0,0 +1,193 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2016-2019 t.e.morgan.
3
+ # Copyright 2021 D.Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'helper'
18
+ require 'ssl'
19
+ require 'date'
20
+ require 'minitest/mock'
21
+ require 'fluent/plugin/out_devo'
22
+ require 'fluent/test/driver/output'
23
+
24
+ class SyslogTlsOutputTest < Test::Unit::TestCase
25
+ include SSLTestHelper
26
+
27
+ def setup
28
+ Fluent::Test.setup
29
+ @driver = nil
30
+ end
31
+
32
+ def driver(conf='')
33
+ @driver ||= Fluent::Test::Driver::Output.new(Fluent::Plugin::SyslogTlsOutput).configure(conf)
34
+ end
35
+
36
+ def sample_record
37
+ {
38
+ "app_name" => "app",
39
+ "hostname" => "host",
40
+ "procid" => $$,
41
+ "msgid" => 1000,
42
+ "message" => "MESSAGE",
43
+ "severity" => "PANIC",
44
+ }
45
+ end
46
+
47
+ def mock_logger(token='TOKEN')
48
+ io = StringIO.new
49
+ io.set_encoding('utf-8')
50
+ ::SyslogTls::Logger.new(io, token)
51
+ end
52
+
53
+ def test_configure
54
+ config = %{
55
+ host syslog.collection.us1.sumologic.com
56
+ port 6514
57
+ client_cert
58
+ client_key
59
+ verify_cert_name true
60
+ token 1234567890
61
+ }
62
+ instance = driver(config).instance
63
+
64
+ assert_equal 'syslog.collection.us1.sumologic.com', instance.host
65
+ assert_equal '6514', instance.port
66
+ assert_equal '', instance.client_cert
67
+ assert_equal '', instance.client_key
68
+ assert_equal true, instance.verify_cert_name
69
+ assert_equal '1234567890', instance.token
70
+ end
71
+
72
+ def test_default_emit
73
+ config = %{
74
+ host syslog.collection.us1.sumologic.com
75
+ port 6514
76
+ client_cert
77
+ client_key
78
+ }
79
+ instance = driver(config).instance
80
+
81
+ time = Time.now
82
+ record = sample_record
83
+ logger = mock_logger(instance.token)
84
+
85
+ instance.stub(:new_logger, logger) do
86
+ instance.process('test', {time.to_i => record})
87
+ end
88
+
89
+ assert_equal "<134>#{time.to_datetime.strftime("%b %d %H:%M:%S")} #{record.to_json.to_s}\n\n", logger.transport.string
90
+ end
91
+
92
+ def test_message_headers_mapping
93
+ config = %{
94
+ host syslog.collection.us1.sumologic.com
95
+ port 6514
96
+ client_cert
97
+ client_key
98
+ token 1234567890
99
+ hostname_key hostname
100
+ procid_key procid
101
+ app_name_key app_name
102
+ msgid_key msgid
103
+ }
104
+ instance = driver(config).instance
105
+
106
+ time = Time.now
107
+ record = sample_record
108
+ logger = mock_logger
109
+
110
+ instance.stub(:new_logger, logger) do
111
+ instance.process('test', {time.to_i => record})
112
+ end
113
+
114
+ assert_true logger.transport.string.start_with?("<134>#{time.to_datetime.strftime("%b %d %H:%M:%S")} host app")
115
+ end
116
+
117
+ def test_message_severity_mapping
118
+ config = %{
119
+ host syslog.collection.us1.sumologic.com
120
+ port 6514
121
+ client_cert
122
+ client_key
123
+ token 1234567890
124
+ severity_key severity
125
+ }
126
+ instance = driver(config).instance
127
+
128
+ time = Time.now
129
+ record = sample_record
130
+ logger = mock_logger
131
+
132
+ instance.stub(:new_logger, logger) do
133
+ instance.process('test', {time.to_i => record})
134
+ end
135
+
136
+ assert_true logger.transport.string.start_with?("<128>")
137
+ end
138
+
139
+ def test_formatter
140
+ config = %{
141
+ host syslog.collection.us1.sumologic.com
142
+ port 6514
143
+ client_cert
144
+ client_key
145
+ token 1234567890
146
+ format out_file
147
+ localtime false
148
+ }
149
+ instance = driver(config).instance
150
+
151
+ time = Time.now
152
+ record = sample_record
153
+ logger = mock_logger
154
+
155
+ instance.stub(:new_logger, logger) do
156
+ instance.process('test', {time.to_i => record})
157
+ end
158
+
159
+ formatted_time = time.dup.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
160
+ assert_equal "<134>#{time.to_datetime.strftime("%b %d %H:%M:%S")} #{formatted_time}\ttest\t#{record.to_json.to_s}\n\n", logger.transport.string
161
+ end
162
+
163
+ def test_ssl
164
+ time = Time.now
165
+ record = sample_record
166
+
167
+ server = ssl_server
168
+ st = Thread.new {
169
+ client = server.accept
170
+ assert_equal "<134>#{time.to_datetime.strftime("%b %d %H:%M:%S")} host app #{record.to_json.to_s}\n", client.gets
171
+ client.close
172
+ }
173
+
174
+ config = %{
175
+ host localhost
176
+ port #{server.addr[1]}
177
+ client_cert
178
+ client_key
179
+ token 1234567890
180
+ hostname_key hostname
181
+ procid_key procid
182
+ app_name_key app_name
183
+ msgid_key msgid
184
+ }
185
+ instance = driver(config).instance
186
+
187
+ SyslogTls::SSLTransport.stub_any_instance(:get_ssl_connection, ssl_client) do
188
+ instance.process('test', {time.to_i => record})
189
+ end
190
+
191
+ st.join
192
+ end
193
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,29 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2021 D.Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'coveralls'
17
+ require 'simplecov'
18
+
19
+ SimpleCov.start
20
+
21
+ Coveralls.wear! if ENV['TRAVIS']
22
+
23
+ require 'test/unit'
24
+ require 'fluent/test'
25
+ require 'minitest/pride'
26
+ require 'minitest/stub_any_instance'
27
+
28
+ require 'webmock/test_unit'
29
+ WebMock.disable_net_connect!