fluent-plugin-devo 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +85 -0
- data/LICENSE +235 -0
- data/README.md +98 -0
- data/Rakefile +24 -0
- data/lib/fluent/plugin/out_devo.rb +151 -0
- data/lib/syslog_tls/facility.rb +47 -0
- data/lib/syslog_tls/logger.rb +83 -0
- data/lib/syslog_tls/lookup_from_const.rb +37 -0
- data/lib/syslog_tls/protocol.rb +137 -0
- data/lib/syslog_tls/severity.rb +31 -0
- data/lib/syslog_tls/ssl_transport.rb +190 -0
- data/lib/syslog_tls/version.rb +19 -0
- data/test/fluent/test_out_devo.rb +193 -0
- data/test/helper.rb +29 -0
- data/test/ssl.rb +51 -0
- data/test/syslog_tls/test_logger.rb +49 -0
- data/test/syslog_tls/test_protocol.rb +151 -0
- data/test/syslog_tls/test_ssl_transport.rb +55 -0
- metadata +173 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# Copyright 2016 Acquia, Inc.
|
2
|
+
# Copyright 2021 Devo .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 Devo .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 Devo .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 = '1.0.0'
|
19
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# Copyright 2016 Acquia, Inc.
|
2
|
+
# Copyright 2016-2019 t.e.morgan.
|
3
|
+
# Copyright 2021 Devo .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 Devo .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!
|