logstash-input-relp 0.1.5 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/NOTICE.TXT +5 -0
- data/README.md +5 -5
- data/lib/logstash/inputs/relp.rb +64 -16
- data/lib/logstash/util/relp.rb +5 -120
- data/logstash-input-relp.gemspec +3 -3
- data/spec/inputs/relp_spec.rb +101 -48
- data/spec/spec_helper.rb +66 -0
- data/spec/support/client.rb +131 -0
- data/spec/support/ssl.rb +27 -0
- metadata +15 -9
- data/.gitignore +0 -4
- data/Rakefile +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf41bf3e9f571773cd7317ccaa639812b8af4b75
|
4
|
+
data.tar.gz: 5b7b8ade54236ee71f4dd1540a81a831b245c02b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c6724f73a866b45ec55f446c732b1fae0c027fda14bb5cf7516be7fd669a078c82d227db9442fd90467b5ad78d3552873df1ba823573facd37f0807ca17ee548
|
7
|
+
data.tar.gz: e616c70e39a338199012442b962b2b3766417b028d5b3ab10f0d0aefcd9bdf26e3e970714eed99973865ac286ec02b78504b93b6cbdbf620e1ecc5aba662f27f
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
## 2.0.0
|
2
|
+
- Plugins were updated to follow the new shutdown semantic, this mainly allows Logstash to instruct input plugins to terminate gracefully,
|
3
|
+
instead of using Thread.raise on the plugins' threads. Ref: https://github.com/elastic/logstash/pull/3895
|
4
|
+
- Dependency on logstash-core update to 2.0
|
5
|
+
|
6
|
+
# 0.2.0
|
7
|
+
- Add support for using RELP over SSL sockets
|
data/NOTICE.TXT
ADDED
data/README.md
CHANGED
@@ -1,19 +1,19 @@
|
|
1
1
|
# Logstash Plugin
|
2
2
|
|
3
|
-
This is a plugin for [Logstash](https://github.com/
|
3
|
+
This is a plugin for [Logstash](https://github.com/elastic/logstash).
|
4
4
|
|
5
5
|
It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
|
6
6
|
|
7
7
|
## Documentation
|
8
8
|
|
9
|
-
Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.
|
9
|
+
Logstash provides infrastructure to automatically generate documentation for this plugin. We use the asciidoc format to write documentation so any comments in the source code will be first converted into asciidoc and then into html. All plugin documentation are placed under one [central location](http://www.elastic.co/guide/en/logstash/current/).
|
10
10
|
|
11
11
|
- For formatting code or config example, you can use the asciidoc `[source,ruby]` directive
|
12
|
-
- For more asciidoc formatting tips, see the excellent reference here https://github.com/
|
12
|
+
- For more asciidoc formatting tips, see the excellent reference here https://github.com/elastic/docs#asciidoc-guide
|
13
13
|
|
14
14
|
## Need Help?
|
15
15
|
|
16
|
-
Need help? Try #logstash on freenode IRC or the logstash
|
16
|
+
Need help? Try #logstash on freenode IRC or the https://discuss.elastic.co/c/logstash discussion forum.
|
17
17
|
|
18
18
|
## Developing
|
19
19
|
|
@@ -83,4 +83,4 @@ Programming is not a required skill. Whatever you've seen about open source and
|
|
83
83
|
|
84
84
|
It is more important to the community that you are able to contribute.
|
85
85
|
|
86
|
-
For more information about contributing, see the [CONTRIBUTING](https://github.com/
|
86
|
+
For more information about contributing, see the [CONTRIBUTING](https://github.com/elastic/logstash/blob/master/CONTRIBUTING.md) file.
|
data/lib/logstash/inputs/relp.rb
CHANGED
@@ -4,8 +4,6 @@ require "logstash/namespace"
|
|
4
4
|
require "logstash/util/relp"
|
5
5
|
require "logstash/util/socket_peer"
|
6
6
|
|
7
|
-
|
8
|
-
|
9
7
|
# Read RELP events over a TCP socket.
|
10
8
|
#
|
11
9
|
# For more information about RELP, see
|
@@ -27,25 +25,77 @@ class LogStash::Inputs::Relp < LogStash::Inputs::Base
|
|
27
25
|
# The port to listen on.
|
28
26
|
config :port, :validate => :number, :required => true
|
29
27
|
|
28
|
+
# Enable SSL (must be set for other `ssl_` options to take effect).
|
29
|
+
config :ssl_enable, :validate => :boolean, :default => false
|
30
|
+
|
31
|
+
# Verify the identity of the other end of the SSL connection against the CA.
|
32
|
+
# For input, sets the field `sslsubject` to that of the client certificate.
|
33
|
+
config :ssl_verify, :validate => :boolean, :default => true
|
34
|
+
|
35
|
+
# The SSL CA certificate, chainfile or CA path. The system CA path is automatically included.
|
36
|
+
config :ssl_cacert, :validate => :path
|
37
|
+
|
38
|
+
# SSL certificate path
|
39
|
+
config :ssl_cert, :validate => :path
|
40
|
+
|
41
|
+
# SSL key path
|
42
|
+
config :ssl_key, :validate => :path
|
43
|
+
|
44
|
+
# SSL key passphrase
|
45
|
+
config :ssl_key_passphrase, :validate => :password, :default => nil
|
46
|
+
|
30
47
|
def initialize(*args)
|
31
48
|
super(*args)
|
32
|
-
@interrupted = false
|
33
49
|
@relp_server = nil
|
34
50
|
end # def initialize
|
35
51
|
|
36
52
|
public
|
37
53
|
def register
|
38
54
|
@logger.info("Starting relp input listener", :address => "#{@host}:#{@port}")
|
39
|
-
|
55
|
+
if @ssl_enable
|
56
|
+
initialize_ssl_context
|
57
|
+
if @ssl_verify == false
|
58
|
+
@logger.warn [
|
59
|
+
"** WARNING ** Detected UNSAFE options in relp input configuration!",
|
60
|
+
"** WARNING ** You have enabled encryption but DISABLED certificate verification.",
|
61
|
+
"** WARNING ** To make sure your data is secure change :ssl_verify to true"
|
62
|
+
].join("\n")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
@relp_server = RelpServer.new(@host, @port,['syslog'], @ssl_context)
|
40
66
|
end # def register
|
41
67
|
|
68
|
+
private
|
69
|
+
def initialize_ssl_context
|
70
|
+
require "openssl"
|
71
|
+
|
72
|
+
@ssl_context = OpenSSL::SSL::SSLContext.new
|
73
|
+
@ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@ssl_cert))
|
74
|
+
@ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@ssl_key),@ssl_key_passphrase.value)
|
75
|
+
if @ssl_verify
|
76
|
+
@cert_store = OpenSSL::X509::Store.new
|
77
|
+
# Load the system default certificate path to the store
|
78
|
+
@cert_store.set_default_paths
|
79
|
+
if !@ssl_cacert.nil?
|
80
|
+
if File.directory?(@ssl_cacert)
|
81
|
+
@cert_store.add_path(@ssl_cacert)
|
82
|
+
else
|
83
|
+
@cert_store.add_file(@ssl_cacert)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
@ssl_context.cert_store = @cert_store
|
87
|
+
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
42
91
|
private
|
43
92
|
def relp_stream(relpserver,socket,output_queue,client_address)
|
44
|
-
|
93
|
+
while !stop?
|
45
94
|
frame = relpserver.syslog_read(socket)
|
46
95
|
@codec.decode(frame["message"]) do |event|
|
47
96
|
decorate(event)
|
48
97
|
event["host"] = client_address
|
98
|
+
event["sslsubject"] ||= socket.peer_cert.subject if @ssl_enable && @ssl_verify
|
49
99
|
output_queue << event
|
50
100
|
end
|
51
101
|
|
@@ -58,8 +108,7 @@ class LogStash::Inputs::Relp < LogStash::Inputs::Base
|
|
58
108
|
|
59
109
|
public
|
60
110
|
def run(output_queue)
|
61
|
-
|
62
|
-
while !@interrupted
|
111
|
+
while !stop?
|
63
112
|
begin
|
64
113
|
# Start a new thread for each connection.
|
65
114
|
Thread.start(@relp_server.accept) do |client|
|
@@ -87,18 +136,17 @@ class LogStash::Inputs::Relp < LogStash::Inputs::Base
|
|
87
136
|
@logger.warn('Relp client trying to open connection with something other than open:'+e.message)
|
88
137
|
rescue Relp::InsufficientCommands
|
89
138
|
@logger.warn('Relp client incapable of syslog')
|
90
|
-
rescue
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
@
|
95
|
-
|
96
|
-
|
139
|
+
rescue Relp::ConnectionClosed
|
140
|
+
@logger.debug('Relp Connection closed')
|
141
|
+
rescue OpenSSL::SSL::SSLError => ssle
|
142
|
+
# NOTE(mrichar1): This doesn't return a useful error message for some reason
|
143
|
+
@logger.error("SSL Error", :exception => ssle,
|
144
|
+
:backtrace => ssle.backtrace)
|
145
|
+
end
|
97
146
|
end # loop
|
98
147
|
end # def run
|
99
148
|
|
100
|
-
def
|
101
|
-
@interrupted = true
|
149
|
+
def stop
|
102
150
|
if @relp_server
|
103
151
|
@relp_server.shutdown rescue nil
|
104
152
|
@relp_server = nil
|
data/lib/logstash/util/relp.rb
CHANGED
@@ -100,7 +100,7 @@ end
|
|
100
100
|
|
101
101
|
class RelpServer < Relp
|
102
102
|
|
103
|
-
def initialize(host,port,required_commands=[])
|
103
|
+
def initialize(host,port,required_commands=[],ssl_context=nil)
|
104
104
|
@logger = Cabin::Channel.get(LogStash)
|
105
105
|
|
106
106
|
@server=true
|
@@ -118,7 +118,10 @@ class RelpServer < Relp
|
|
118
118
|
:host => host, :port => port)
|
119
119
|
raise
|
120
120
|
end
|
121
|
-
|
121
|
+
if ssl_context
|
122
|
+
@server = OpenSSL::SSL::SSLServer.new(@server, ssl_context)
|
123
|
+
end
|
124
|
+
@logger.info("Started #{ssl_context ? 'SSL-enabled ' : ''}RELP Server", :host => host, :port => port)
|
122
125
|
end
|
123
126
|
|
124
127
|
def accept
|
@@ -213,121 +216,3 @@ class RelpServer < Relp
|
|
213
216
|
end
|
214
217
|
|
215
218
|
end
|
216
|
-
|
217
|
-
#This is only used by the tests; any problems here are not as important as elsewhere
|
218
|
-
class RelpClient < Relp
|
219
|
-
|
220
|
-
def initialize(host,port,required_commands = [],buffer_size = 128,
|
221
|
-
retransmission_timeout=10)
|
222
|
-
@logger = Cabin::Channel.get(LogStash)
|
223
|
-
@logger.info? and @logger.info("Starting RELP client", :host => host, :port => port)
|
224
|
-
@server = false
|
225
|
-
@buffer = Hash.new
|
226
|
-
|
227
|
-
@buffer_size = buffer_size
|
228
|
-
@retransmission_timeout = retransmission_timeout
|
229
|
-
|
230
|
-
#These are things that are part of the basic protocol, but only valid in one direction (rsp, close etc.)
|
231
|
-
@basic_relp_commands = ['serverclose','rsp']#TODO: check for others
|
232
|
-
|
233
|
-
#These are extra commands that we require, otherwise refuse the connection
|
234
|
-
@required_relp_commands = required_commands
|
235
|
-
|
236
|
-
@socket=TCPSocket.new(host,port)
|
237
|
-
|
238
|
-
#This'll start the automatic frame numbering
|
239
|
-
@lasttxnr = 0
|
240
|
-
|
241
|
-
offer=Hash.new
|
242
|
-
offer['command'] = 'open'
|
243
|
-
offer['message'] = 'relp_version=' + RelpVersion + "\n"
|
244
|
-
offer['message'] += 'relp_software=' + RelpSoftware + "\n"
|
245
|
-
offer['message'] += 'commands=' + @required_relp_commands.join(',')#TODO: add optional ones
|
246
|
-
self.frame_write(@socket, offer)
|
247
|
-
response_frame = self.frame_read(@socket)
|
248
|
-
if response_frame['message'][0,3] != '200'
|
249
|
-
raise RelpError,response_frame['message']
|
250
|
-
end
|
251
|
-
|
252
|
-
response=Hash[*response_frame['message'][7..-1].scan(/^(.*)=(.*)$/).flatten]
|
253
|
-
if response['relp_version'].nil?
|
254
|
-
#if no version specified, relp spec says we must close connection
|
255
|
-
self.close()
|
256
|
-
raise RelpError, 'No relp_version specified; offer: '
|
257
|
-
+ response_frame['message'][6..-1].scan(/^(.*)=(.*)$/).flatten
|
258
|
-
|
259
|
-
#subtracting one array from the other checks to see if all elements in @required_relp_commands are present in the offer
|
260
|
-
elsif ! (@required_relp_commands - response['commands'].split(',')).empty?
|
261
|
-
#if it can't receive syslog it's useless to us; close the connection
|
262
|
-
self.close()
|
263
|
-
raise InsufficientCommands, response['commands'] + ' offered, require '
|
264
|
-
+ @required_relp_commands.join(',')
|
265
|
-
end
|
266
|
-
#If we've got this far with no problems, we're good to go
|
267
|
-
@logger.info? and @logger.info("Connection establish with server")
|
268
|
-
|
269
|
-
#This thread deals with responses that come back
|
270
|
-
reader = Thread.start do
|
271
|
-
loop do
|
272
|
-
f = self.frame_read(@socket)
|
273
|
-
if f['command'] == 'rsp' && f['message'] == '200 OK'
|
274
|
-
@buffer.delete(f['txnr'])
|
275
|
-
elsif f['command'] == 'rsp' && f['message'][0,1] == '5'
|
276
|
-
#TODO: What if we get an error for something we're already retransmitted due to timeout?
|
277
|
-
new_txnr = self.frame_write(@socket, @buffer[f['txnr']])
|
278
|
-
@buffer[new_txnr] = @buffer[f['txnr']]
|
279
|
-
@buffer.delete(f['txnr'])
|
280
|
-
elsif f['command'] == 'serverclose' || f['txnr'] == @close_txnr
|
281
|
-
break
|
282
|
-
else
|
283
|
-
#Don't know what's going on if we get here, but it can't be good
|
284
|
-
raise RelpError#TODO: raising errors like this makes no sense
|
285
|
-
end
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
#While this one deals with frames for which we get no reply
|
290
|
-
Thread.start do
|
291
|
-
old_buffer = Hash.new
|
292
|
-
loop do
|
293
|
-
#This returns old txnrs that are still present
|
294
|
-
(@buffer.keys & old_buffer.keys).each do |txnr|
|
295
|
-
new_txnr = self.frame_write(@socket, @buffer[txnr])
|
296
|
-
@buffer[new_txnr] = @buffer[txnr]
|
297
|
-
@buffer.delete(txnr)
|
298
|
-
end
|
299
|
-
old_buffer = @buffer
|
300
|
-
sleep @retransmission_timeout
|
301
|
-
end
|
302
|
-
end
|
303
|
-
end
|
304
|
-
|
305
|
-
#TODO: have a way to get back unacked messages on close
|
306
|
-
def close
|
307
|
-
frame = Hash.new
|
308
|
-
frame['command'] = 'close'
|
309
|
-
@close_txnr=self.frame_write(@socket, frame)
|
310
|
-
#TODO: ought to properly wait for a reply etc. The serverclose will make it work though
|
311
|
-
sleep @retransmission_timeout
|
312
|
-
@socket.close#TODO: shutdown?
|
313
|
-
return @buffer
|
314
|
-
end
|
315
|
-
|
316
|
-
def syslog_write(logline)
|
317
|
-
|
318
|
-
#If the buffer is already full, wait until a gap opens up
|
319
|
-
sleep 0.1 until @buffer.length<@buffer_size
|
320
|
-
|
321
|
-
frame = Hash.new
|
322
|
-
frame['command'] = 'syslog'
|
323
|
-
frame['message'] = logline
|
324
|
-
|
325
|
-
txnr = self.frame_write(@socket, frame)
|
326
|
-
@buffer[txnr] = frame
|
327
|
-
end
|
328
|
-
|
329
|
-
def nexttxnr
|
330
|
-
@lasttxnr += 1
|
331
|
-
end
|
332
|
-
|
333
|
-
end
|
data/logstash-input-relp.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
|
3
3
|
s.name = 'logstash-input-relp'
|
4
|
-
s.version = '0.1
|
4
|
+
s.version = '2.0.1'
|
5
5
|
s.licenses = ['Apache License (2.0)']
|
6
6
|
s.summary = "Read RELP events over a TCP socket."
|
7
7
|
s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program"
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.require_paths = ["lib"]
|
12
12
|
|
13
13
|
# Files
|
14
|
-
s.files =
|
14
|
+
s.files = Dir['lib/**/*','spec/**/*','vendor/**/*','*.gemspec','*.md','CONTRIBUTORS','Gemfile','LICENSE','NOTICE.TXT']
|
15
15
|
|
16
16
|
# Tests
|
17
17
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
|
21
21
|
|
22
22
|
# Gem dependencies
|
23
|
-
s.add_runtime_dependency "logstash-core",
|
23
|
+
s.add_runtime_dependency "logstash-core", ">= 2.0.0.snapshot", "< 3.0.0"
|
24
24
|
|
25
25
|
s.add_runtime_dependency 'logstash-codec-plain'
|
26
26
|
s.add_development_dependency 'logstash-devutils'
|
data/spec/inputs/relp_spec.rb
CHANGED
@@ -1,60 +1,113 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
1
|
+
# encoding: utf-8
|
2
|
+
require_relative "../spec_helper"
|
3
|
+
require_relative "../support/ssl"
|
4
|
+
|
5
|
+
describe LogStash::Inputs::Relp do
|
6
|
+
|
7
|
+
before do
|
8
|
+
srand(RSpec.configuration.seed)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "registration and close" do
|
12
|
+
|
13
|
+
it "should register without errors" do
|
14
|
+
input = LogStash::Plugin.lookup("input", "relp").new("port" => 1234)
|
15
|
+
expect {input.register}.to_not raise_error
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "when interrupting the plugin" do
|
21
|
+
|
22
|
+
let(:port) { rand(1024..65532) }
|
23
|
+
|
24
|
+
it_behaves_like "an interruptible input plugin" do
|
25
|
+
let(:config) { { "port" => port } }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "multiple client connections" do
|
30
|
+
|
31
|
+
let(:nclients) { rand(200) }
|
32
|
+
let(:nevents) { 100 }
|
33
|
+
let(:port) { 5512 }
|
34
|
+
|
35
|
+
let(:conf) do
|
36
|
+
<<-CONFIG
|
37
|
+
input {
|
38
|
+
relp {
|
39
|
+
type => "blah"
|
40
|
+
port => #{port}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
CONFIG
|
44
|
+
end
|
45
|
+
|
46
|
+
let(:clients) { setup_clients(nclients, port) }
|
47
|
+
|
48
|
+
let(:events) do
|
49
|
+
input(conf, (nevents*nclients)) do
|
50
|
+
nevents.times do |value|
|
51
|
+
clients.each_with_index do |client, index|
|
52
|
+
client.syslog_write("Hello from client#{index}")
|
53
|
+
end
|
54
|
+
end
|
24
55
|
end
|
25
|
-
event_count.times.collect { queue.pop }
|
26
56
|
end
|
27
57
|
|
28
|
-
|
29
|
-
|
58
|
+
it "should do multiple connections" do
|
59
|
+
nclients.times do |client_id|
|
60
|
+
expect(events).to have(nevents).with("Hello from client#{client_id}")
|
61
|
+
end
|
30
62
|
end
|
31
63
|
end
|
32
64
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
65
|
+
describe "SSL support" do
|
66
|
+
|
67
|
+
let(:nevents) { 100 }
|
68
|
+
let(:certificate) { RelpTest.certificate }
|
69
|
+
let(:port) { 5513 }
|
70
|
+
|
71
|
+
let(:conf) do
|
72
|
+
<<-CONFIG
|
73
|
+
input {
|
74
|
+
relp {
|
75
|
+
type => "blah"
|
76
|
+
port => #{port}
|
77
|
+
ssl_enable => true
|
78
|
+
ssl_verify => false
|
79
|
+
ssl_cert => "#{certificate.ssl_cert}"
|
80
|
+
ssl_key => "#{certificate.ssl_key}"
|
81
|
+
}
|
82
|
+
}
|
83
|
+
CONFIG
|
84
|
+
end
|
85
|
+
|
86
|
+
let(:client) { RelpClient.new("0.0.0.0", port, ["syslog"], {:ssl => true}) }
|
87
|
+
|
88
|
+
let(:events) do
|
89
|
+
input(conf, nevents) do
|
90
|
+
nevents.times do
|
91
|
+
client.syslog_write("Hello from client")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "registration and close" do
|
97
|
+
|
98
|
+
it "should register without errors" do
|
99
|
+
input = LogStash::Plugin.lookup("input", "relp").new("port" => 1235, "ssl_enable" => true,
|
100
|
+
"ssl_cert" => certificate.ssl_cert,
|
101
|
+
"ssl_key" => certificate.ssl_key)
|
102
|
+
expect {input.register}.to_not raise_error
|
52
103
|
end
|
53
104
|
|
54
|
-
(event_count * 2).times.map{queue.pop}
|
55
105
|
end
|
56
106
|
|
57
|
-
|
58
|
-
|
107
|
+
it "should generated the events as expected" do
|
108
|
+
expect(events).to have(nevents).with("Hello from client")
|
109
|
+
end
|
110
|
+
|
59
111
|
end
|
112
|
+
|
60
113
|
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/devutils/rspec/spec_helper"
|
3
|
+
require "logstash/inputs/relp"
|
4
|
+
require "logstash/util/relp"
|
5
|
+
require "socket"
|
6
|
+
require "support/client"
|
7
|
+
|
8
|
+
module RelpHelpers
|
9
|
+
|
10
|
+
def setup_clients(number_of_clients, port)
|
11
|
+
number_of_clients.times.inject([]) do |clients|
|
12
|
+
clients << RelpClient.new("0.0.0.0", port, ["syslog"])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def filter(events, message)
|
17
|
+
events.select{|event| event["message"] == message }
|
18
|
+
end
|
19
|
+
|
20
|
+
def input(config, size, &block)
|
21
|
+
pipeline = LogStash::Pipeline.new(config)
|
22
|
+
queue = Queue.new
|
23
|
+
|
24
|
+
pipeline.instance_eval do
|
25
|
+
# create closure to capture queue
|
26
|
+
@output_func = lambda { |event| queue << event }
|
27
|
+
|
28
|
+
# output_func is now a method, call closure
|
29
|
+
def output_func(event)
|
30
|
+
@output_func.call(event)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
pipeline_thread = Thread.new { pipeline.run }
|
35
|
+
sleep 0.1 while !pipeline.ready?
|
36
|
+
|
37
|
+
block.call
|
38
|
+
sleep 0.1 while queue.size != size
|
39
|
+
|
40
|
+
result = size.times.inject([]) do |acc|
|
41
|
+
acc << queue.pop
|
42
|
+
end
|
43
|
+
|
44
|
+
pipeline.shutdown
|
45
|
+
pipeline_thread.join
|
46
|
+
|
47
|
+
result
|
48
|
+
end # def input
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
RSpec.configure do |c|
|
53
|
+
c.include RelpHelpers
|
54
|
+
end
|
55
|
+
|
56
|
+
RSpec::Matchers.define :have do |nevents|
|
57
|
+
|
58
|
+
match do |events|
|
59
|
+
filter(events, @pattern).size == nevents
|
60
|
+
end
|
61
|
+
|
62
|
+
chain :with do |pattern|
|
63
|
+
@pattern = pattern
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "socket"
|
3
|
+
|
4
|
+
#This is only used by the tests; any problems here are not as important as elsewhere
|
5
|
+
class RelpClient < Relp
|
6
|
+
|
7
|
+
def initialize(host,port,required_commands = [], options={})
|
8
|
+
buffer_size = options[:buffer_size] || 128
|
9
|
+
retransmission_timeout = options[:retransmission_timeout] || 10
|
10
|
+
ssl = options[:ssl] || false
|
11
|
+
@logger = Cabin::Channel.get(LogStash)
|
12
|
+
@logger.info? and @logger.info("Starting RELP client", :host => host, :port => port)
|
13
|
+
@server = false
|
14
|
+
@buffer = Hash.new
|
15
|
+
|
16
|
+
@buffer_size = buffer_size
|
17
|
+
@retransmission_timeout = retransmission_timeout
|
18
|
+
|
19
|
+
#These are things that are part of the basic protocol, but only valid in one direction (rsp, close etc.)
|
20
|
+
@basic_relp_commands = ['serverclose','rsp']#TODO: check for others
|
21
|
+
|
22
|
+
#These are extra commands that we require, otherwise refuse the connection
|
23
|
+
@required_relp_commands = required_commands
|
24
|
+
|
25
|
+
@socket=TCPSocket.new(host,port)
|
26
|
+
|
27
|
+
if ssl
|
28
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
29
|
+
ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_NONE)
|
30
|
+
@socket = OpenSSL::SSL::SSLSocket.new(@socket, ctx).tap do |socket|
|
31
|
+
socket.sync_close = true
|
32
|
+
socket.connect
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
#This'll start the automatic frame numbering
|
37
|
+
@lasttxnr = 0
|
38
|
+
|
39
|
+
offer=Hash.new
|
40
|
+
offer['command'] = 'open'
|
41
|
+
offer['message'] = 'relp_version=' + RelpVersion + "\n"
|
42
|
+
offer['message'] += 'relp_software=' + RelpSoftware + "\n"
|
43
|
+
offer['message'] += 'commands=' + @required_relp_commands.join(',')#TODO: add optional ones
|
44
|
+
self.frame_write(@socket, offer)
|
45
|
+
response_frame = self.frame_read(@socket)
|
46
|
+
if response_frame['message'][0,3] != '200'
|
47
|
+
raise RelpError,response_frame['message']
|
48
|
+
end
|
49
|
+
|
50
|
+
response=Hash[*response_frame['message'][7..-1].scan(/^(.*)=(.*)$/).flatten]
|
51
|
+
if response['relp_version'].nil?
|
52
|
+
#if no version specified, relp spec says we must close connection
|
53
|
+
self.close()
|
54
|
+
raise RelpError, 'No relp_version specified; offer: '
|
55
|
+
+ response_frame['message'][6..-1].scan(/^(.*)=(.*)$/).flatten
|
56
|
+
|
57
|
+
#subtracting one array from the other checks to see if all elements in @required_relp_commands are present in the offer
|
58
|
+
elsif ! (@required_relp_commands - response['commands'].split(',')).empty?
|
59
|
+
#if it can't receive syslog it's useless to us; close the connection
|
60
|
+
self.close()
|
61
|
+
raise InsufficientCommands, response['commands'] + ' offered, require '
|
62
|
+
+ @required_relp_commands.join(',')
|
63
|
+
end
|
64
|
+
#If we've got this far with no problems, we're good to go
|
65
|
+
@logger.info? and @logger.info("Connection establish with server")
|
66
|
+
|
67
|
+
#This thread deals with responses that come back
|
68
|
+
reader = Thread.start do
|
69
|
+
loop do
|
70
|
+
f = self.frame_read(@socket)
|
71
|
+
if f['command'] == 'rsp' && f['message'] == '200 OK'
|
72
|
+
@buffer.delete(f['txnr'])
|
73
|
+
elsif f['command'] == 'rsp' && f['message'][0,1] == '5'
|
74
|
+
#TODO: What if we get an error for something we're already retransmitted due to timeout?
|
75
|
+
new_txnr = self.frame_write(@socket, @buffer[f['txnr']])
|
76
|
+
@buffer[new_txnr] = @buffer[f['txnr']]
|
77
|
+
@buffer.delete(f['txnr'])
|
78
|
+
elsif f['command'] == 'serverclose' || f['txnr'] == @close_txnr
|
79
|
+
break
|
80
|
+
else
|
81
|
+
#Don't know what's going on if we get here, but it can't be good
|
82
|
+
raise RelpError#TODO: raising errors like this makes no sense
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
#While this one deals with frames for which we get no reply
|
88
|
+
Thread.start do
|
89
|
+
old_buffer = Hash.new
|
90
|
+
loop do
|
91
|
+
#This returns old txnrs that are still present
|
92
|
+
(@buffer.keys & old_buffer.keys).each do |txnr|
|
93
|
+
new_txnr = self.frame_write(@socket, @buffer[txnr])
|
94
|
+
@buffer[new_txnr] = @buffer[txnr]
|
95
|
+
@buffer.delete(txnr)
|
96
|
+
end
|
97
|
+
old_buffer = @buffer
|
98
|
+
sleep @retransmission_timeout
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
#TODO: have a way to get back unacked messages on close
|
104
|
+
def close
|
105
|
+
frame = Hash.new
|
106
|
+
frame['command'] = 'close'
|
107
|
+
@close_txnr=self.frame_write(@socket, frame)
|
108
|
+
#TODO: ought to properly wait for a reply etc. The serverclose will make it work though
|
109
|
+
sleep @retransmission_timeout
|
110
|
+
@socket.close#TODO: shutdown?
|
111
|
+
return @buffer
|
112
|
+
end
|
113
|
+
|
114
|
+
def syslog_write(logline)
|
115
|
+
|
116
|
+
#If the buffer is already full, wait until a gap opens up
|
117
|
+
sleep 0.1 until @buffer.length<@buffer_size
|
118
|
+
|
119
|
+
frame = Hash.new
|
120
|
+
frame['command'] = 'syslog'
|
121
|
+
frame['message'] = logline
|
122
|
+
|
123
|
+
txnr = self.frame_write(@socket, frame)
|
124
|
+
@buffer[txnr] = frame
|
125
|
+
end
|
126
|
+
|
127
|
+
def nexttxnr
|
128
|
+
@lasttxnr += 1
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
data/spec/support/ssl.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "stud/temporary"
|
3
|
+
|
4
|
+
module RelpTest
|
5
|
+
|
6
|
+
class Certicate
|
7
|
+
attr_reader :ssl_key, :ssl_cert
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@ssl_cert = Stud::Temporary.pathname("ssl_certificate")
|
11
|
+
@ssl_key = Stud::Temporary.pathname("ssl_key")
|
12
|
+
|
13
|
+
system("openssl req -x509 -batch -nodes -newkey rsa:2048 -keyout #{ssl_key} -out #{ssl_cert} -subj /CN=localhost > /dev/null 2>&1")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def certificate
|
19
|
+
Certicate.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def random_port
|
23
|
+
rand(2000..10000)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
metadata
CHANGED
@@ -1,24 +1,24 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-input-relp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Elastic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
16
|
- - '>='
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version:
|
18
|
+
version: 2.0.0.snapshot
|
19
19
|
- - <
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version:
|
21
|
+
version: 3.0.0
|
22
22
|
name: logstash-core
|
23
23
|
prerelease: false
|
24
24
|
type: :runtime
|
@@ -26,10 +26,10 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - '>='
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version:
|
29
|
+
version: 2.0.0.snapshot
|
30
30
|
- - <
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: 3.0.0
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|
35
35
|
requirements:
|
@@ -64,16 +64,19 @@ executables: []
|
|
64
64
|
extensions: []
|
65
65
|
extra_rdoc_files: []
|
66
66
|
files:
|
67
|
-
- .
|
67
|
+
- CHANGELOG.md
|
68
68
|
- CONTRIBUTORS
|
69
69
|
- Gemfile
|
70
70
|
- LICENSE
|
71
|
+
- NOTICE.TXT
|
71
72
|
- README.md
|
72
|
-
- Rakefile
|
73
73
|
- lib/logstash/inputs/relp.rb
|
74
74
|
- lib/logstash/util/relp.rb
|
75
75
|
- logstash-input-relp.gemspec
|
76
76
|
- spec/inputs/relp_spec.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
- spec/support/client.rb
|
79
|
+
- spec/support/ssl.rb
|
77
80
|
homepage: http://www.elastic.co/guide/en/logstash/current/index.html
|
78
81
|
licenses:
|
79
82
|
- Apache License (2.0)
|
@@ -96,9 +99,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
99
|
version: '0'
|
97
100
|
requirements: []
|
98
101
|
rubyforge_project:
|
99
|
-
rubygems_version: 2.
|
102
|
+
rubygems_version: 2.4.8
|
100
103
|
signing_key:
|
101
104
|
specification_version: 4
|
102
105
|
summary: Read RELP events over a TCP socket.
|
103
106
|
test_files:
|
104
107
|
- spec/inputs/relp_spec.rb
|
108
|
+
- spec/spec_helper.rb
|
109
|
+
- spec/support/client.rb
|
110
|
+
- spec/support/ssl.rb
|
data/.gitignore
DELETED