fluent-plugin-syslog-tls-long-timeout 0.6.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,82 @@
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_relative 'protocol'
17
+ require_relative 'ssl_transport'
18
+
19
+ module SyslogTls
20
+ class Logger
21
+ attr_reader :token
22
+ attr_accessor :transport
23
+
24
+ # Logger accepts transport which should implement IO methods
25
+ # close, closed? and write
26
+ def initialize(transport, token=nil)
27
+ @transport = transport
28
+ @default_header = SyslogTls::Header.new
29
+ @default_structured_data = SyslogTls::StructuredData.new(token)
30
+ end
31
+
32
+ # Sets default facility for each message
33
+ def facility(val)
34
+ @default_header.facility = val
35
+ end
36
+
37
+ # Sets default hostname for each message
38
+ def hostname(val)
39
+ @default_header.hostname = val
40
+ end
41
+
42
+ # Sets default app_name for each message
43
+ def app_name(val)
44
+ @default_header.app_name = val
45
+ end
46
+
47
+ # Sets default procid for message
48
+ def procid(val)
49
+ @default_header.procid = val
50
+ end
51
+
52
+ # Check if IO is closed
53
+ def closed?
54
+ transport && transport.closed?
55
+ end
56
+
57
+ def close
58
+ transport.close
59
+ end
60
+
61
+ # Send log message with severity to syslog
62
+ def log(severity, message, time: nil)
63
+ time ||= Time.now
64
+
65
+ m = SyslogTls::Message.new
66
+
67
+ # Include authentication header
68
+ m.structured_data << @default_structured_data
69
+
70
+ # Adjust header with current timestamp and severity
71
+ m.header = @default_header.dup
72
+ m.header.severity = severity
73
+ m.header.timestamp = time
74
+
75
+ yield m.header if block_given?
76
+
77
+ m.msg = message
78
+
79
+ transport.write(m.to_s)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,36 @@
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
+ module SyslogTls
16
+ module LookupFromConst
17
+ def setup_constants(dst)
18
+ constants.each do |pri|
19
+ cval = const_get pri
20
+
21
+ dst[pri] = cval
22
+ dst[pri.downcase] = cval
23
+
24
+ dst[:"LOG_#{pri.to_s}"] = cval
25
+ dst[:"LOG_#{pri.downcase.to_s}"] = cval
26
+ const_set :"LOG_#{pri.to_s}", cval
27
+
28
+ dst[pri.to_s] = cval
29
+ dst[pri.downcase.to_s] = cval
30
+
31
+ dst[cval] = cval
32
+ dst[cval.to_s] = cval
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,140 @@
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 :facility, :severity, :version, :timestamp, :hostname, :app_name, :procid, :msgid
29
+
30
+ FACILITIES = {}
31
+ SEVERITIES = {}
32
+
33
+ Facility.setup_constants FACILITIES
34
+ Severity.setup_constants SEVERITIES
35
+
36
+ def initialize
37
+ @timestamp = Time.now
38
+ @severity = 'INFO'
39
+ @facility = 'LOCAL0'
40
+ @version = 1
41
+ @hostname = NIL_VALUE
42
+ @app_name = NIL_VALUE
43
+ @procid = NIL_VALUE
44
+ @msgid = NIL_VALUE
45
+ end
46
+
47
+ def timestamp=(val)
48
+ raise ArgumentError.new("Must provide Time object value instead: #{val.inspect}") unless val.is_a?(Time)
49
+ @timestamp = val
50
+ end
51
+
52
+ def facility=(val)
53
+ raise ArgumentError.new("Invalid facility value: #{val.inspect}") unless FACILITIES.key?(val)
54
+ @facility = val
55
+ end
56
+
57
+ def severity=(val)
58
+ raise ArgumentError.new("Invalid severity value: #{val.inspect}") unless SEVERITIES.key?(val)
59
+ @severity = val
60
+ end
61
+
62
+ # Priority value is calculated by first multiplying the Facility
63
+ # number by 8 and then adding the numerical value of the Severity.
64
+ def pri
65
+ FACILITIES[facility] * 8 + SEVERITIES[severity]
66
+ end
67
+
68
+ def assemble
69
+ [
70
+ "<#{pri}>#{version}",
71
+ timestamp.to_datetime.rfc3339,
72
+ hostname,
73
+ app_name,
74
+ procid,
75
+ msgid
76
+ ].join(' ')
77
+ end
78
+
79
+ def to_s
80
+ assemble
81
+ end
82
+ end
83
+
84
+ # Structured data field
85
+ class StructuredData
86
+ attr_accessor :id, :data
87
+
88
+ def initialize(id)
89
+ @id = id
90
+ @data = {}
91
+ end
92
+
93
+ # Format data structured data to
94
+ # [id k="v" ...]
95
+ def assemble
96
+ return NIL_VALUE unless id
97
+ parts = [id]
98
+ data.each do |k, v|
99
+ # Characters ", ] and \ must be escaped to prevent any parsing errors
100
+ v = v.gsub(/(\"|\]|\\)/) { |match| '\\' + match }
101
+ parts << "#{k}=\"#{v}\""
102
+ end
103
+ "[#{parts.join(' ')}]"
104
+ end
105
+
106
+ def to_s
107
+ assemble
108
+ end
109
+ end
110
+
111
+ # Message represents full message that can be sent to syslog
112
+ class Message
113
+ attr_accessor :header, :structured_data, :msg
114
+
115
+ def initialize
116
+ @msg = ''
117
+ @structured_data = []
118
+ @header = Header.new
119
+ end
120
+
121
+ def assemble
122
+ # Start with header
123
+ out = [header.to_s]
124
+ # Add all structured data
125
+ if structured_data.length > 0
126
+ out << structured_data.map(&:to_s).join('')
127
+ else
128
+ out << NIL_VALUE
129
+ end
130
+ # Add message
131
+ out << msg if msg.length > 0
132
+ # Message must end with new line delimiter
133
+ out.join(' ') + "\n"
134
+ end
135
+
136
+ def to_s
137
+ assemble
138
+ end
139
+ end
140
+ 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,90 @@
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 'socket'
16
+ require 'openssl'
17
+
18
+ module SyslogTls
19
+ # Supports SSL connection to remote host
20
+ class SSLTransport
21
+ attr_accessor :socket
22
+
23
+ attr_reader :host, :port, :ca_cert, :cert, :key, :ssl_version
24
+
25
+ attr_writer :retries
26
+
27
+ def initialize(host, port, ca_cert: 'system', cert: nil, key: nil, ssl_version: :TLSv1_2, max_retries: 1)
28
+ @ca_cert = ca_cert
29
+ @host = host
30
+ @port = port
31
+ @cert = cert
32
+ @key = key
33
+ @ssl_version = ssl_version
34
+ @retries = max_retries
35
+ connect
36
+ end
37
+
38
+ def connect
39
+ @socket = get_ssl_connection
40
+ @socket.connect
41
+ end
42
+
43
+ def get_ssl_connection
44
+ tcp = TCPSocket.new(host, port)
45
+
46
+ ctx = OpenSSL::SSL::SSLContext.new
47
+ ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
48
+ ctx.ssl_version = ssl_version
49
+
50
+ case ca_cert
51
+ when true, 'true', 'system'
52
+ # use system certs, same as openssl cli
53
+ ctx.cert_store = OpenSSL::X509::Store.new
54
+ ctx.cert_store.set_default_paths
55
+ when false, 'false'
56
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
57
+ when %r{/$} # ends in /
58
+ ctx.ca_path = ca_cert
59
+ when String
60
+ ctx.ca_file = ca_cert
61
+ end
62
+
63
+ ctx.cert = OpenSSL::X509::Certificate.new(File.read(cert)) if cert
64
+ ctx.key = OpenSSL::PKey::read(File.read(key)) if key
65
+ socket = OpenSSL::SSL::SSLSocket.new(tcp, ctx)
66
+ socket.sync_close = true
67
+ socket
68
+ end
69
+
70
+ # Allow to retry on failed writes
71
+ def write(s)
72
+ begin
73
+ retry_id ||= 0
74
+ @socket.send(:write, s)
75
+ rescue => e
76
+ if (retry_id += 1) < @retries
77
+ connect
78
+ retry
79
+ else
80
+ raise e
81
+ end
82
+ end
83
+ end
84
+
85
+ # Forward any methods directly to SSLSocket
86
+ def method_missing(method_sym, *arguments, &block)
87
+ @socket.send(method_sym, *arguments, &block)
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,18 @@
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
+ module SyslogTls
17
+ VERSION = '0.6.0'
18
+ end
@@ -0,0 +1,200 @@
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 'helper'
17
+ require 'ssl'
18
+ require 'date'
19
+ require 'minitest/mock'
20
+ require 'fluent/plugin/out_syslog_tls'
21
+
22
+ class SyslogTlsOutputTest < Test::Unit::TestCase
23
+ include SSLTestHelper
24
+
25
+ def setup
26
+ Fluent::Test.setup
27
+ @driver = nil
28
+ end
29
+
30
+ def driver(tag='test', conf='')
31
+ @driver ||= Fluent::Test::OutputTestDriver.new(Fluent::SyslogTlsOutput, tag).configure(conf)
32
+ end
33
+
34
+ def sample_record
35
+ {
36
+ "app_name" => "app",
37
+ "hostname" => "host",
38
+ "procid" => $$,
39
+ "msgid" => 1000,
40
+ "message" => "MESSAGE",
41
+ "severity" => "PANIC",
42
+ }
43
+ end
44
+
45
+ def mock_logger(token='TOKEN')
46
+ io = StringIO.new
47
+ io.set_encoding('utf-8')
48
+ logger = ::SyslogTls::Logger.new(io, token)
49
+ end
50
+
51
+ def test_configure
52
+ config = %{
53
+ host syslog.collection.us1.sumologic.com
54
+ port 6514
55
+ cert
56
+ key
57
+ token 1234567890
58
+ }
59
+ instance = driver('test', config).instance
60
+
61
+ assert_equal 'syslog.collection.us1.sumologic.com', instance.host
62
+ assert_equal '6514', instance.port
63
+ assert_equal '', instance.cert
64
+ assert_equal '', instance.key
65
+ assert_equal '1234567890', instance.token
66
+ end
67
+
68
+ def test_default_emit
69
+ config = %{
70
+ host syslog.collection.us1.sumologic.com
71
+ port 6514
72
+ cert
73
+ key
74
+ }
75
+ instance = driver('test', config).instance
76
+
77
+ time = Time.now
78
+ record = sample_record
79
+ logger = mock_logger(instance.token)
80
+
81
+ instance.stub(:new_logger, logger) do
82
+ chain = Minitest::Mock.new
83
+ chain.expect(:next, nil)
84
+ instance.emit('test', {time.to_i => record}, chain)
85
+ end
86
+
87
+ assert_equal "<134>1 #{time.to_datetime.rfc3339} - - - - - #{record.to_json.to_s}\n\n", logger.transport.string
88
+ end
89
+
90
+ def test_message_headers_mapping
91
+ config = %{
92
+ host syslog.collection.us1.sumologic.com
93
+ port 6514
94
+ cert
95
+ key
96
+ token 1234567890
97
+ hostname_key hostname
98
+ procid_key procid
99
+ app_name_key app_name
100
+ msgid_key msgid
101
+ }
102
+ instance = driver('test', config).instance
103
+
104
+ time = Time.now
105
+ record = sample_record
106
+ logger = mock_logger
107
+
108
+ instance.stub(:new_logger, logger) do
109
+ chain = Minitest::Mock.new
110
+ chain.expect(:next, nil)
111
+ instance.emit('test', {time.to_i => record}, chain)
112
+ end
113
+
114
+ assert_true logger.transport.string.start_with?("<134>1 #{time.to_datetime.rfc3339} host app #{$$} 1000 [TOKEN]")
115
+ end
116
+
117
+ def test_message_severity_mapping
118
+ config = %{
119
+ host syslog.collection.us1.sumologic.com
120
+ port 6514
121
+ cert
122
+ key
123
+ token 1234567890
124
+ severity_key severity
125
+ }
126
+ instance = driver('test', config).instance
127
+
128
+ time = Time.now
129
+ record = sample_record
130
+ logger = mock_logger
131
+
132
+ instance.stub(:new_logger, logger) do
133
+ chain = Minitest::Mock.new
134
+ chain.expect(:next, nil)
135
+ instance.emit('test', {time.to_i => record}, chain)
136
+ end
137
+
138
+ assert_true logger.transport.string.start_with?("<128>1")
139
+ end
140
+
141
+ def test_formatter
142
+ config = %{
143
+ host syslog.collection.us1.sumologic.com
144
+ port 6514
145
+ cert
146
+ key
147
+ token 1234567890
148
+ format out_file
149
+ utc true
150
+ }
151
+ instance = driver('test', config).instance
152
+
153
+ time = Time.now
154
+ record = sample_record
155
+ logger = mock_logger
156
+
157
+ instance.stub(:new_logger, logger) do
158
+ chain = Minitest::Mock.new
159
+ chain.expect(:next, nil)
160
+ instance.emit('test', {time.to_i => record}, chain)
161
+ end
162
+
163
+ formatted_time = time.dup.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
164
+ assert_equal "<134>1 #{time.to_datetime.rfc3339} - - - - [TOKEN] #{formatted_time}\ttest\t#{record.to_json.to_s}\n\n", logger.transport.string
165
+ end
166
+
167
+ def test_ssl
168
+ time = Time.now
169
+ record = sample_record
170
+
171
+ server = ssl_server
172
+ st = Thread.new {
173
+ client = server.accept
174
+ assert_equal "<134>1 #{time.to_datetime.rfc3339} host app #{$$} 1000 [1234567890] #{record.to_json.to_s}\n", client.gets
175
+ client.close
176
+ }
177
+
178
+ config = %{
179
+ host localhost
180
+ port #{server.addr[1]}
181
+ cert
182
+ key
183
+ token 1234567890
184
+ hostname_key hostname
185
+ procid_key procid
186
+ app_name_key app_name
187
+ msgid_key msgid
188
+ }
189
+ instance = driver('test', config).instance
190
+
191
+ chain = Minitest::Mock.new
192
+ chain.expect(:next, nil)
193
+
194
+ SyslogTls::SSLTransport.stub_any_instance(:get_ssl_connection, ssl_client) do
195
+ instance.emit('test', {time.to_i => record}, chain)
196
+ end
197
+
198
+ st.join
199
+ end
200
+ end