fluent-plugin-syslog-tls-with-backoff-test 2.1.0

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,145 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2016 t.e.morgan.
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 'date'
17
+
18
+ require_relative 'facility'
19
+ require_relative 'severity'
20
+
21
+ # Syslog protocol https://tools.ietf.org/html/rfc5424
22
+ module SyslogTls
23
+ # RFC defined nil value
24
+ NIL_VALUE = '-'
25
+
26
+ # All headers by specification wrapped in single object
27
+ class Header
28
+ attr_accessor :version, :hostname, :app_name, :procid, :msgid
29
+ attr_reader :facility, :severity, :timestamp
30
+
31
+ FACILITIES = {}
32
+ SEVERITIES = {}
33
+
34
+ Facility.setup_constants FACILITIES
35
+ Severity.setup_constants SEVERITIES
36
+
37
+ def initialize
38
+ @timestamp = Time.now
39
+ @severity = 'INFO'
40
+ @facility = 'LOCAL0'
41
+ @version = 1
42
+ @hostname = NIL_VALUE
43
+ @app_name = NIL_VALUE
44
+ @procid = NIL_VALUE
45
+ @msgid = NIL_VALUE
46
+ end
47
+
48
+ def timestamp=(val)
49
+ raise ArgumentError.new("Must provide Time object value instead: #{val.inspect}") unless val.is_a?(Time)
50
+ @timestamp = val
51
+ end
52
+
53
+ def facility=(val)
54
+ raise ArgumentError.new("Invalid facility value: #{val.inspect}") unless FACILITIES.key?(val)
55
+ @facility = val
56
+ end
57
+
58
+ def severity=(val)
59
+ raise ArgumentError.new("Invalid severity value: #{val.inspect}") unless SEVERITIES.key?(val)
60
+ @severity = val
61
+ end
62
+
63
+ # Priority value is calculated by first multiplying the Facility
64
+ # number by 8 and then adding the numerical value of the Severity.
65
+ def pri
66
+ FACILITIES[facility] * 8 + SEVERITIES[severity]
67
+ end
68
+
69
+ def assemble
70
+ [
71
+ "<#{pri}>#{version}",
72
+ timestamp.to_datetime.rfc3339,
73
+ hostname,
74
+ app_name,
75
+ procid,
76
+ msgid
77
+ ].join(' ')
78
+ end
79
+
80
+ def to_s
81
+ assemble
82
+ end
83
+ end
84
+
85
+ # Structured data field
86
+ class StructuredData
87
+ attr_accessor :id, :data
88
+
89
+ def initialize(id)
90
+ @id = id
91
+ @data = {}
92
+ end
93
+
94
+ # Format data structured data to
95
+ # [id k="v" ...]
96
+ def assemble
97
+ return NIL_VALUE unless id
98
+ parts = [id]
99
+ data.each do |k, v|
100
+ # Characters ", ] and \ must be escaped to prevent any parsing errors
101
+ v = v.gsub(/(\"|\]|\\)/) { |match| '\\' + match }
102
+ parts << "#{k}=\"#{v}\""
103
+ end
104
+ "[#{parts.join(' ')}]"
105
+ end
106
+
107
+ def to_s
108
+ assemble
109
+ end
110
+ end
111
+
112
+ # Message represents full message that can be sent to syslog
113
+ class Message
114
+ attr_accessor :structured_data, :msg
115
+ attr_writer :header
116
+
117
+ def initialize
118
+ @msg = ''
119
+ @structured_data = []
120
+ end
121
+
122
+ def header
123
+ @header ||= Header.new
124
+ end
125
+
126
+ def assemble
127
+ # Start with header
128
+ out = [header.to_s]
129
+ # Add all structured data
130
+ if structured_data.length > 0
131
+ out << structured_data.map(&:to_s).join('')
132
+ else
133
+ out << NIL_VALUE
134
+ end
135
+ # Add message
136
+ out << msg if msg.length > 0
137
+ # Message must end with new line delimiter
138
+ out.join(' ') + "\n"
139
+ end
140
+
141
+ def to_s
142
+ assemble
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,30 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require_relative 'lookup_from_const'
16
+
17
+ module SyslogTls
18
+ module Severity
19
+ extend LookupFromConst
20
+ EMERG = PANIC = 0
21
+ ALERT = 1
22
+ CRIT = 2
23
+ ERR = ERROR = 3
24
+ WARN = WARNING = 4
25
+ NOTICE = 5
26
+ INFO = 6
27
+ DEBUG = 7
28
+ NONE = 10
29
+ end
30
+ end
@@ -0,0 +1,208 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2016-2023 t.e.morgan.
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 'socket'
17
+ require 'openssl'
18
+ require 'pp'
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: :TLS1_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
+ timwnow = Time.now
48
+ @socket = get_ssl_connection
49
+ begin
50
+ begin
51
+ @socket.connect_nonblock
52
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
53
+ select_with_timeout(@socket, :connect_read) && retry
54
+ rescue IO::WaitWritable
55
+ select_with_timeout(@socket, :connect_write) && retry
56
+ end
57
+ rescue Errno::ETIMEDOUT
58
+ raise 'Socket timeout during connect'
59
+ end
60
+ @last_write = Time.now if idle_timeout
61
+ end
62
+
63
+ def get_tcp_connection
64
+ tcp = nil
65
+
66
+ family = Socket::Constants::AF_UNSPEC
67
+ sock_type = Socket::Constants::SOCK_STREAM
68
+ addr_info = Socket.getaddrinfo(host, port, family, sock_type, nil, nil, false).first
69
+ _, port, _, address, family, sock_type = addr_info
70
+
71
+ begin
72
+ sock_addr = Socket.sockaddr_in(port, address)
73
+ tcp = Socket.new(family, sock_type, 0)
74
+ tcp.setsockopt(Socket::SOL_SOCKET, Socket::Constants::SO_REUSEADDR, true)
75
+ tcp.setsockopt(Socket::SOL_SOCKET, Socket::Constants::SO_REUSEPORT, true)
76
+ tcp.connect_nonblock(sock_addr)
77
+ rescue Errno::EINPROGRESS
78
+ select_with_timeout(tcp, :connect_write)
79
+ begin
80
+ tcp.connect_nonblock(sock_addr)
81
+ rescue Errno::EISCONN
82
+ # all good
83
+ rescue SystemCallError
84
+ tcp.close rescue nil
85
+ raise
86
+ end
87
+ rescue SystemCallError
88
+ tcp.close rescue nil
89
+ raise
90
+ end
91
+
92
+ tcp.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
93
+ tcp
94
+ end
95
+
96
+ def get_ssl_connection
97
+ tcp = get_tcp_connection
98
+
99
+ ctx = OpenSSL::SSL::SSLContext.new
100
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
101
+ ctx.min_version = ssl_version
102
+
103
+ ctx.verify_hostname = verify_cert_name != false
104
+
105
+ case ca_cert
106
+ when true, 'true', 'system'
107
+ # use system certs, same as openssl cli
108
+ ctx.cert_store = OpenSSL::X509::Store.new
109
+ ctx.cert_store.set_default_paths
110
+ when false, 'false'
111
+ ctx.verify_hostname = false
112
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
113
+ when %r{/$} # ends in /
114
+ ctx.ca_path = ca_cert
115
+ when String
116
+ ctx.ca_file = ca_cert
117
+ end
118
+
119
+ ctx.cert = OpenSSL::X509::Certificate.new(File.read(client_cert)) if client_cert
120
+ ctx.key = OpenSSL::PKey::read(File.read(client_key)) if client_key
121
+ socket = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
122
+ socket.hostname = host
123
+ socket.sync_close = true
124
+ socket
125
+ end
126
+
127
+ # Allow to retry on failed writes
128
+ def write(s)
129
+ if idle_timeout
130
+ if (t=Time.now) > @last_write + idle_timeout
131
+ @socket.close rescue nil
132
+ connect
133
+ else
134
+ @last_write = t
135
+ end
136
+ end
137
+ begin
138
+ retry_id ||= 0
139
+ do_write(s)
140
+ rescue => e
141
+ if (retry_id += 1) < @retries
142
+ @socket.close rescue nil
143
+ connect
144
+ retry
145
+ else
146
+ raise e
147
+ end
148
+ end
149
+ end
150
+
151
+ def do_write(data)
152
+ data.force_encoding('BINARY') # so we can break in the middle of multi-byte characters
153
+ loop do
154
+ sent = 0
155
+ begin
156
+ sent = @socket.write_nonblock(data)
157
+ rescue OpenSSL::SSL::SSLError, Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitWritable => e
158
+ if e.is_a?(OpenSSL::SSL::SSLError) && e.message !~ /write would block/
159
+ raise e
160
+ else
161
+ select_with_timeout(@socket, :write) && retry
162
+ end
163
+ end
164
+
165
+ break if sent >= data.size
166
+ data = data[sent, data.size]
167
+ end
168
+ end
169
+
170
+ def select_with_timeout(tcp, type)
171
+ host_ip_port = host + ":" + port.to_s
172
+ case type
173
+ when :connect_read
174
+ args = [[tcp], nil, nil, CONNECT_TIMEOUT]
175
+ when :connect_write
176
+ args = [nil, [tcp], nil, CONNECT_TIMEOUT]
177
+ # when :read
178
+ # args = [[tcp], nil, nil, READ_TIMEOUT]
179
+ when :write
180
+ args = [nil, [tcp], nil, WRITE_TIMEOUT]
181
+ else
182
+ raise "Unknown select type #{type}"
183
+ end
184
+ if type.to_s == "connect_write"
185
+ if can_write(host_ip_port) == 1
186
+ io_select_return = IO.select(*args)
187
+ ready_sockets, _, _ = io_select_return
188
+ if !ready_sockets.empty?
189
+ reset_tries(host_ip_port)
190
+ io_select_return
191
+ else
192
+ increase_retry(host_ip_port)
193
+ io_select_return || raise("Socket timeout during #{type}")
194
+ end
195
+ # else
196
+ # raise("Failed to write #{type}")
197
+ end
198
+ else
199
+ IO.select(*args) || raise("Socket timeout during #{type}")
200
+ end
201
+ end
202
+
203
+ # Forward any methods directly to SSLSocket
204
+ def method_missing(method_sym, *arguments, &block)
205
+ @socket.send(method_sym, *arguments, &block)
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,18 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2016-2019 t.e.morgan.
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
+ module SyslogTls
17
+ VERSION = '2.1.0'
18
+ end
@@ -0,0 +1,192 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ # Copyright 2016-2019 t.e.morgan.
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 'helper'
17
+ require 'ssl'
18
+ require 'date'
19
+ require 'minitest/mock'
20
+ require 'fluent/plugin/out_syslog_tls'
21
+ require 'fluent/test/driver/output'
22
+
23
+ class SyslogTlsOutputTest < Test::Unit::TestCase
24
+ include SSLTestHelper
25
+
26
+ def setup
27
+ Fluent::Test.setup
28
+ @driver = nil
29
+ end
30
+
31
+ def driver(conf='')
32
+ @driver ||= Fluent::Test::Driver::Output.new(Fluent::Plugin::SyslogTlsOutput).configure(conf)
33
+ end
34
+
35
+ def sample_record
36
+ {
37
+ "app_name" => "app",
38
+ "hostname" => "host",
39
+ "procid" => $$,
40
+ "msgid" => 1000,
41
+ "message" => "MESSAGE",
42
+ "severity" => "PANIC",
43
+ }
44
+ end
45
+
46
+ def mock_logger(token='TOKEN')
47
+ io = StringIO.new
48
+ io.set_encoding('utf-8')
49
+ ::SyslogTls::Logger.new(io, token)
50
+ end
51
+
52
+ def test_configure
53
+ config = %{
54
+ host syslog.collection.us1.sumologic.com
55
+ port 6514
56
+ client_cert
57
+ client_key
58
+ verify_cert_name true
59
+ token 1234567890
60
+ }
61
+ instance = driver(config).instance
62
+
63
+ assert_equal 'syslog.collection.us1.sumologic.com', instance.host
64
+ assert_equal '6514', instance.port
65
+ assert_equal '', instance.client_cert
66
+ assert_equal '', instance.client_key
67
+ assert_equal true, instance.verify_cert_name
68
+ assert_equal '1234567890', instance.token
69
+ end
70
+
71
+ def test_default_emit
72
+ config = %{
73
+ host syslog.collection.us1.sumologic.com
74
+ port 6514
75
+ client_cert
76
+ client_key
77
+ }
78
+ instance = driver(config).instance
79
+
80
+ time = Time.now
81
+ record = sample_record
82
+ logger = mock_logger(instance.token)
83
+
84
+ instance.stub(:new_logger, logger) do
85
+ instance.process('test', {time.to_i => record})
86
+ end
87
+
88
+ assert_equal "<134>1 #{time.to_datetime.rfc3339} - - - - - #{record.to_json.to_s}\n\n", logger.transport.string
89
+ end
90
+
91
+ def test_message_headers_mapping
92
+ config = %{
93
+ host syslog.collection.us1.sumologic.com
94
+ port 6514
95
+ client_cert
96
+ client_key
97
+ token 1234567890
98
+ hostname_key hostname
99
+ procid_key procid
100
+ app_name_key app_name
101
+ msgid_key msgid
102
+ }
103
+ instance = driver(config).instance
104
+
105
+ time = Time.now
106
+ record = sample_record
107
+ logger = mock_logger
108
+
109
+ instance.stub(:new_logger, logger) do
110
+ instance.process('test', {time.to_i => record})
111
+ end
112
+
113
+ assert_true logger.transport.string.start_with?("<134>1 #{time.to_datetime.rfc3339} host app #{$$} 1000 [TOKEN]")
114
+ end
115
+
116
+ def test_message_severity_mapping
117
+ config = %{
118
+ host syslog.collection.us1.sumologic.com
119
+ port 6514
120
+ client_cert
121
+ client_key
122
+ token 1234567890
123
+ severity_key severity
124
+ }
125
+ instance = driver(config).instance
126
+
127
+ time = Time.now
128
+ record = sample_record
129
+ logger = mock_logger
130
+
131
+ instance.stub(:new_logger, logger) do
132
+ instance.process('test', {time.to_i => record})
133
+ end
134
+
135
+ assert_true logger.transport.string.start_with?("<128>1")
136
+ end
137
+
138
+ def test_formatter
139
+ config = %{
140
+ host syslog.collection.us1.sumologic.com
141
+ port 6514
142
+ client_cert
143
+ client_key
144
+ token 1234567890
145
+ format out_file
146
+ localtime false
147
+ }
148
+ instance = driver(config).instance
149
+
150
+ time = Time.now
151
+ record = sample_record
152
+ logger = mock_logger
153
+
154
+ instance.stub(:new_logger, logger) do
155
+ instance.process('test', {time.to_i => record})
156
+ end
157
+
158
+ formatted_time = time.dup.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
159
+ assert_equal "<134>1 #{time.to_datetime.rfc3339} - - - - [TOKEN] #{formatted_time}\ttest\t#{record.to_json.to_s}\n\n", logger.transport.string
160
+ end
161
+
162
+ def test_ssl
163
+ time = Time.now
164
+ record = sample_record
165
+
166
+ server = ssl_server
167
+ st = Thread.new {
168
+ client = server.accept
169
+ assert_equal "<134>1 #{time.to_datetime.rfc3339} host app #{$$} 1000 [1234567890] #{record.to_json.to_s}\n", client.gets
170
+ client.close
171
+ }
172
+
173
+ config = %{
174
+ host localhost
175
+ port #{server.addr[1]}
176
+ client_cert
177
+ client_key
178
+ token 1234567890
179
+ hostname_key hostname
180
+ procid_key procid
181
+ app_name_key app_name
182
+ msgid_key msgid
183
+ }
184
+ instance = driver(config).instance
185
+
186
+ SyslogTls::SSLTransport.stub_any_instance(:get_ssl_connection, ssl_client) do
187
+ instance.process('test', {time.to_i => record})
188
+ end
189
+
190
+ st.join
191
+ end
192
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,25 @@
1
+ # Copyright 2016 Acquia, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'simplecov'
16
+
17
+ SimpleCov.start
18
+
19
+ require 'test/unit'
20
+ require 'fluent/test'
21
+ require 'minitest/pride'
22
+ require 'minitest/stub_any_instance'
23
+
24
+ require 'webmock/test_unit'
25
+ WebMock.disable_net_connect!
data/test/ssl.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'socket'
2
+ require 'openssl'
3
+
4
+ module SSLTestHelper
5
+ def ssl_server(min_version: nil, max_version: nil)
6
+ @ssl_server ||= begin
7
+ tcp_server = TCPServer.new("localhost", 33000 + Random.rand(1000))
8
+ ssl_context = OpenSSL::SSL::SSLContext.new
9
+ ssl_context.cert = certificate
10
+ ssl_context.key = rsa_key
11
+ ssl_context.min_version = min_version if min_version
12
+ ssl_context.max_version = max_version if max_version
13
+ OpenSSL::SSL::SSLServer.new(tcp_server, ssl_context)
14
+ end
15
+ end
16
+
17
+ def ssl_client
18
+ tcp = TCPSocket.new("localhost", ssl_server.addr[1])
19
+ ctx = OpenSSL::SSL::SSLContext.new
20
+ ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_NONE)
21
+ ctx.cert = certificate
22
+ ctx.key = rsa_key
23
+ OpenSSL::SSL::SSLSocket.new(tcp, ctx)
24
+ end
25
+
26
+ def rsa_key
27
+ @rsa_key ||= OpenSSL::PKey::RSA.new(2048)
28
+ end
29
+
30
+ def certificate
31
+ @cert ||= begin
32
+ cert = OpenSSL::X509::Certificate.new
33
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse("/C=BE/O=Test/OU=Test/CN=Test")
34
+ cert.not_before = Time.now
35
+ cert.not_after = Time.now + 365 * 24 * 60 * 60
36
+ cert.public_key = rsa_key.public_key
37
+ cert.serial = 0x0
38
+ cert.version = 2
39
+
40
+ ef = OpenSSL::X509::ExtensionFactory.new
41
+ ef.subject_certificate = cert
42
+ ef.issuer_certificate = cert
43
+ cert.extensions = [
44
+ ef.create_extension("basicConstraints","CA:TRUE", true),
45
+ ef.create_extension("subjectKeyIdentifier", "hash"),
46
+ # ef.create_extension("keyUsage", "cRLSign,keyCertSign", true),
47
+ ]
48
+ cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
49
+ cert.sign(rsa_key, OpenSSL::Digest::SHA1.new)
50
+ cert
51
+ end
52
+ end
53
+ end