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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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