fluent-plugin-syslog-tls 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,74 @@
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, :cert, :key, :ssl_version
24
+
25
+ attr_writer :retries
26
+
27
+ def initialize(host, port, cert: nil, key: nil, ssl_version: :TLSv1_2, max_retries: 1)
28
+ @host = host
29
+ @port = port
30
+ @cert = cert
31
+ @key = key
32
+ @ssl_version = ssl_version
33
+ @retries = max_retries
34
+ connect
35
+ end
36
+
37
+ def connect
38
+ @socket = get_ssl_connection
39
+ @socket.connect
40
+ end
41
+
42
+ def get_ssl_connection
43
+ tcp = TCPSocket.new(host, port)
44
+
45
+ ctx = OpenSSL::SSL::SSLContext.new
46
+ ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
47
+ ctx.ssl_version = ssl_version
48
+
49
+ ctx.cert = OpenSSL::X509::Certificate.new(File.open(cert)) if cert
50
+ ctx.key = OpenSSL::PKey::RSA.new(File.open(key)) if key
51
+ OpenSSL::SSL::SSLSocket.new(tcp, ctx)
52
+ end
53
+
54
+ # Allow to retry on failed writes
55
+ def write(s)
56
+ begin
57
+ retry_id ||= 0
58
+ @socket.send(:write, s)
59
+ rescue => e
60
+ if (retry_id += 1) < @retries
61
+ connect
62
+ retry
63
+ else
64
+ raise e
65
+ end
66
+ end
67
+ end
68
+
69
+ # Forward any methods directly to SSLSocket
70
+ def method_missing(method_sym, *arguments, &block)
71
+ @socket.send(method_sym, *arguments, &block)
72
+ end
73
+ end
74
+ 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.5.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