logstash-input-tcp 2.0.5 → 3.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c7064c954a185e8f91e06bd314abb3daacc80d8
4
- data.tar.gz: ca2dd0c7f7fdac14a7ea33027a62bd3dccfcc497
3
+ metadata.gz: bc8a144097d1b6d3ee89c67da0a9598c20816dc0
4
+ data.tar.gz: db3e1d7399ee28bcdbad61d343c589249287ad3d
5
5
  SHA512:
6
- metadata.gz: b7ca6d3d05531210c046bf3bb4e093839679434721c0d6e5d5bacc2f881786322689af56c296dd312daabd51febf615c20b6710f41dee00d5252bcbcaf3e45af
7
- data.tar.gz: 6f37a0e1cf9c291e00fa4d6c0f24eb008375fc961c931ec3bcb345e2256a5f274cf2ce64d0198f28a7befd7c0e51e4cb594635929beefc7f6685c8a501297fa5
6
+ metadata.gz: d8b9f22e646a94b0624d961075589d48899595e9aaedb4111fcc1cf6118a9173366f3caf6f7b0315c8048d0a6d850ee6da0319eea9f413413d5a6d2511d09c12
7
+ data.tar.gz: a5d0c419104ca62126ad38c4e3a0a83a81083df5febe5184a4c7edff44893a9b4141589830a99bbfaff365652cbcb43f8cdeb11e88d3fe6b341e36b32911a175
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 3.0.0
2
+ - Deprecate ssl_cacert as it's confusing, does it job but when willing to add a chain of certificated the name and behaviour is a bit confusing.
3
+ - Add ssl_extra_chain_certs that allows you to specify a list of certificates path that will be added to the CAStore.
4
+ - Make ssl_verify=true as a default value, if using ssl and performing validation is not reasonable as security might be compromised.
5
+ - Add tests to verify behaviour under different SSL connection circumstances.
6
+ - Fixes #3 and #9.
7
+
8
+ ## 2.1.0
9
+ - Added the receiving port in the event payload, fixes #4
10
+
1
11
  ## 2.0.5
2
12
  - Fixed malformed SSL crashing Logstash, see https://github.com/logstash-plugins/logstash-input-tcp/pull/25
3
13
 
@@ -35,10 +35,10 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
35
35
 
36
36
  # Verify the identity of the other end of the SSL connection against the CA.
37
37
  # For input, sets the field `sslsubject` to that of the client certificate.
38
- config :ssl_verify, :validate => :boolean, :default => false
38
+ config :ssl_verify, :validate => :boolean, :default => true
39
39
 
40
40
  # The SSL CA certificate, chainfile or CA path. The system CA path is automatically included.
41
- config :ssl_cacert, :validate => :path
41
+ config :ssl_cacert, :validate => :path, :deprecated => "This setting is deprecated in favor of extra_chain_cert as it sets a more clear expectation to add more X509 certificates to the store"
42
42
 
43
43
  # SSL certificate path
44
44
  config :ssl_cert, :validate => :path
@@ -49,6 +49,10 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
49
49
  # SSL key passphrase
50
50
  config :ssl_key_passphrase, :validate => :password, :default => nil
51
51
 
52
+ # An Array of extra X509 certificates to be added to the certificate chain.
53
+ # Useful when the CA chain is not necessary in the system store.
54
+ config :ssl_extra_chain_certs, :validate => :array, :default => []
55
+
52
56
  def initialize(*args)
53
57
  super(*args)
54
58
 
@@ -126,7 +130,7 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
126
130
  def run_client(output_queue)
127
131
  while !stop?
128
132
  self.client_socket = new_client_socket
129
- handle_socket(client_socket, client_socket.peeraddr[3], output_queue, @codec.clone)
133
+ handle_socket(client_socket, client_socket.peeraddr[3], client_socket.peeraddr[1], output_queue, @codec.clone)
130
134
  end
131
135
  ensure
132
136
  # catch all rescue nil on close to discard any close errors or invalid socket
@@ -137,17 +141,18 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
137
141
  Thread.new(output_queue, socket) do |q, s|
138
142
  begin
139
143
  @logger.debug? && @logger.debug("Accepted connection", :client => s.peer, :server => "#{@host}:#{@port}")
140
- handle_socket(s, s.peeraddr[3], q, @codec.clone)
144
+ handle_socket(s, s.peeraddr[3], s.peeraddr[1], q, @codec.clone)
141
145
  ensure
142
146
  delete_connection_socket(s)
143
147
  end
144
148
  end
145
149
  end
146
150
 
147
- def handle_socket(socket, client_address, output_queue, codec)
151
+ def handle_socket(socket, client_address, client_port, output_queue, codec)
148
152
  while !stop?
149
153
  codec.decode(read(socket)) do |event|
150
154
  event["host"] ||= client_address
155
+ event["port"] ||= client_port
151
156
  event["sslsubject"] ||= socket.peer_cert.subject if @ssl_enable && @ssl_verify
152
157
  decorate(event)
153
158
  output_queue << event
@@ -170,12 +175,28 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
170
175
 
171
176
  codec.respond_to?(:flush) && codec.flush do |event|
172
177
  event["host"] ||= client_address
178
+ event["port"] ||= client_port
173
179
  event["sslsubject"] ||= socket.peer_cert.subject if @ssl_enable && @ssl_verify
174
180
  decorate(event)
175
181
  output_queue << event
176
182
  end
177
183
  end
178
184
 
185
+ private
186
+ def client_thread(output_queue, socket)
187
+ Thread.new(output_queue, socket) do |q, s|
188
+ begin
189
+ @logger.debug? && @logger.debug("Accepted connection", :client => s.peer, :server => "#{@host}:#{@port}")
190
+ handle_socket(s, s.peeraddr[3], s.peeraddr[1], q, @codec.clone)
191
+ rescue Interrupted
192
+ s.close rescue nil
193
+ ensure
194
+ @client_threads_lock.synchronize{@client_threads.delete(Thread.current)}
195
+ end
196
+ end
197
+ end
198
+
199
+ private
179
200
  def server?
180
201
  @mode == "server"
181
202
  end
@@ -192,15 +213,7 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
192
213
  @ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@ssl_cert))
193
214
  @ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@ssl_key),@ssl_key_passphrase)
194
215
  if @ssl_verify
195
- @cert_store = OpenSSL::X509::Store.new
196
- # Load the system default certificate path to the store
197
- @cert_store.set_default_paths
198
- if File.directory?(@ssl_cacert)
199
- @cert_store.add_path(@ssl_cacert)
200
- else
201
- @cert_store.add_file(@ssl_cacert)
202
- end
203
- @ssl_context.cert_store = @cert_store
216
+ @ssl_context.cert_store = load_cert_store
204
217
  @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
205
218
  end
206
219
  rescue => e
@@ -211,9 +224,22 @@ class LogStash::Inputs::Tcp < LogStash::Inputs::Base
211
224
  @ssl_context
212
225
  end
213
226
 
227
+ def load_cert_store
228
+ cert_store = OpenSSL::X509::Store.new
229
+ cert_store.set_default_paths
230
+ if File.directory?(@ssl_cacert)
231
+ cert_store.add_path(@ssl_cacert)
232
+ else
233
+ cert_store.add_file(@ssl_cacert)
234
+ end if @ssl_cacert
235
+ @ssl_extra_chain_certs.each do |cert|
236
+ cert_store.add_file(cert)
237
+ end
238
+ cert_store
239
+ end
240
+
214
241
  def new_server_socket
215
242
  @logger.info("Starting tcp input listener", :address => "#{@host}:#{@port}")
216
-
217
243
  begin
218
244
  socket = TCPServer.new(@host, @port)
219
245
  rescue Errno::EADDRINUSE
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-input-tcp'
4
- s.version = '2.0.5'
4
+ s.version = '3.0.0'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "Read 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"
@@ -26,6 +26,9 @@ Gem::Specification.new do |s|
26
26
  s.add_runtime_dependency 'logstash-codec-line'
27
27
  s.add_runtime_dependency 'logstash-codec-json'
28
28
  s.add_runtime_dependency 'logstash-codec-json_lines'
29
+
29
30
  s.add_development_dependency 'logstash-devutils'
31
+ s.add_development_dependency 'flores', '~> 0.0.6'
32
+ s.add_development_dependency 'stud', '~> 0.0.22'
30
33
  end
31
34
 
@@ -4,7 +4,10 @@ require "socket"
4
4
  require "timeout"
5
5
  require "logstash/json"
6
6
  require "logstash/inputs/tcp"
7
- require 'stud/try'
7
+ require "stud/try"
8
+ require "flores/pki"
9
+ require "openssl"
10
+
8
11
  require_relative "../spec_helper"
9
12
 
10
13
  describe LogStash::Inputs::Tcp do
@@ -251,7 +254,6 @@ describe LogStash::Inputs::Tcp do
251
254
  socket.flush
252
255
  end
253
256
  end
254
-
255
257
  socket.close rescue nil
256
258
 
257
259
  result
@@ -269,8 +271,154 @@ describe LogStash::Inputs::Tcp do
269
271
  end
270
272
  end
271
273
 
272
- end
274
+ it "should add the host and port to the generated event" do
275
+ events.each do |event|
276
+ expect(event["host"]).to eq("127.0.0.1")
277
+ expect(event["port"]).to be_an(Fixnum)
278
+ end
279
+ end
280
+
281
+ describe "ssl" do
282
+
283
+ let(:certificate) { helper.certificate }
284
+
285
+ subject(:input) { LogStash::Plugin.lookup("input", "tcp").new(config) }
286
+
287
+ let(:config) do
288
+ { "host" => "0.0.0.0", "port" => port, "ssl_verify" => false,
289
+ "ssl_enable" => true, "ssl_cert" => certificate[0].path, "ssl_key" => certificate[1].path }
290
+ end
291
+
292
+ let(:events) do
293
+
294
+ socket = Stud::try(5.times) do
295
+ ssl_context = OpenSSL::SSL::SSLContext.new
296
+ socket = TCPSocket.new("127.0.0.1", port)
297
+ OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
298
+ end
299
+
300
+ result = helper.pipelineless_input(subject, nevents) do
301
+ socket.connect
302
+ nevents.times do |i|
303
+ socket.puts("msg #{i}")
304
+ socket.flush
305
+ end
306
+ end
307
+ socket.close rescue nil
308
+
309
+ result
310
+ end
311
+
312
+ it "should receive events" do
313
+ expect(events.size).to be(nevents)
314
+ end
315
+
316
+ describe "when ssl_verify is on" do
317
+
318
+ let(:chain_of_certificates) { helper.chain_of_certificates }
319
+
320
+ let(:ssl_context) do
321
+ ssl_context = OpenSSL::SSL::SSLContext.new
322
+ ssl_context.cert = OpenSSL::X509::Certificate.new(client_certificate)
323
+ ssl_context.key = OpenSSL::PKey::RSA.new(client_key)
324
+ ssl_context
325
+ end
326
+
327
+ context "and the verification fails" do
328
+
329
+ let(:config) do
330
+ { "host" => "0.0.0.0", "port" => port,
331
+ "ssl_enable" => true, "ssl_verify" => true,
332
+ "ssl_cert" => chain_of_certificates[:a_cert].path, "ssl_key" => chain_of_certificates[:a_key].path }
333
+ end
334
+
335
+ let(:client_certificate) { File.read(chain_of_certificates[:b_cert].path) }
336
+ let(:client_key) { File.read(chain_of_certificates[:b_key].path) }
337
+
338
+ let(:socket) do
339
+ client = TCPSocket.new("127.0.0.1", port)
340
+ OpenSSL::SSL::SSLSocket.new(client, ssl_context)
341
+ end
273
342
 
343
+ it "should raise an exception when connecting" do
344
+ helper.pipelineless_input(subject, 0) do
345
+ expect { socket.connect }.to raise_error
346
+ socket.close rescue nil
347
+ end
348
+ end
349
+ end
350
+ context "and using the root CA" do
351
+
352
+ let(:config) do
353
+ { "host" => "0.0.0.0", "port" => port,
354
+ "ssl_enable" => true, "ssl_verify" => true,
355
+ "ssl_cert" => chain_of_certificates[:a_cert].path, "ssl_key" => chain_of_certificates[:a_key].path,
356
+ "ssl_cacert" => chain_of_certificates[:root_ca].path }
357
+ end
358
+
359
+ let(:client_certificate) { File.read(chain_of_certificates[:aa_cert].path) }
360
+ let(:client_key) { File.read(chain_of_certificates[:aa_key].path) }
361
+
362
+ let(:events) do
363
+ socket = Stud::try(5.times) do
364
+ socket = TCPSocket.new("127.0.0.1", port)
365
+ OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
366
+ end
367
+ result = helper.pipelineless_input(subject, nevents) do
368
+ socket.connect
369
+ nevents.times do |i|
370
+ socket.puts("msg #{i}")
371
+ socket.flush
372
+ end
373
+ end
374
+ socket.close rescue nil
375
+
376
+ result
377
+ end
378
+
379
+ it "should receive events" do
380
+ expect(events.size).to be(nevents)
381
+ end
382
+
383
+ end
384
+
385
+ context "using an extra chain of certificates" do
386
+
387
+ let(:config) do
388
+ { "host" => "0.0.0.0", "port" => port,
389
+ "ssl_enable" => true, "ssl_verify" => true,
390
+ "ssl_cert" => chain_of_certificates[:b_cert].path, "ssl_key" => chain_of_certificates[:b_key].path,
391
+ "ssl_extra_chain_certs" => [ chain_of_certificates[:root_ca].path, chain_of_certificates[:a_cert].path, chain_of_certificates[:b_cert].path ] }
392
+ end
393
+
394
+ let(:client_certificate) { File.read(chain_of_certificates[:c_cert].path) }
395
+ let(:client_key) { File.read(chain_of_certificates[:c_key].path) }
396
+
397
+ let(:events) do
398
+ socket = Stud::try(5.times) do
399
+ socket = TCPSocket.new("127.0.0.1", port)
400
+ OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
401
+ end
402
+ result = helper.pipelineless_input(subject, nevents) do
403
+ socket.connect
404
+ nevents.times do |i|
405
+ socket.puts("msg #{i}")
406
+ socket.flush
407
+ end
408
+ end
409
+ socket.close rescue nil
410
+
411
+ result
412
+ end
413
+
414
+ it "should receive events" do
415
+ expect(events.size).to be(nevents)
416
+ end
417
+ end
418
+ end
419
+
420
+ end
421
+ end
274
422
  it_behaves_like "an interruptible input plugin" do
275
423
  let(:config) { { "port" => port } }
276
424
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
  require "logstash/devutils/rspec/spec_helper"
3
+ require "tempfile"
4
+ require "stud/temporary"
3
5
 
4
6
  # this has been taken from the udp input, it should be DRYed
5
7
 
@@ -19,4 +21,71 @@ class TcpHelpers
19
21
  input_thread.join
20
22
  result
21
23
  end
24
+
25
+ def certificate
26
+ certificate, key = Flores::PKI.generate("CN=localhost", { :key_size => 2048 })
27
+ [new_temp_file('cert', certificate), new_temp_file('key', key)]
28
+ end
29
+
30
+ def chain_of_certificates
31
+ root_ca, root_key = build_root_ca
32
+ a_cert, a_key = build_certificate(root_ca, root_key, "A_Cert")
33
+ aa_cert, aa_key = build_certificate(root_ca, root_key, "AA_Cert")
34
+ b_cert, b_key = build_certificate(a_cert, a_key, "B_Cert")
35
+ c_cert, c_key = build_certificate(b_cert, b_key, "C_Cert")
36
+ { :root_ca => new_temp_file('', root_ca), :root_key => new_temp_file('', root_key),
37
+ :a_cert => new_temp_file('', a_cert), :a_key => new_temp_file('', a_key),
38
+ :aa_cert => new_temp_file('', aa_cert), :aa_key => new_temp_file('', aa_key),
39
+ :b_cert => new_temp_file('', b_cert), :b_key => new_temp_file('', b_key),
40
+ :c_cert => new_temp_file('', c_cert), :c_key => new_temp_file('', c_key)}
41
+ end
42
+
43
+ private
44
+
45
+ def new_temp_file(name, data)
46
+ file = Stud::Temporary.file
47
+ file.write(data)
48
+ file.rewind
49
+ file
50
+ end
51
+
52
+ def build_certificate(root_ca, root_key=nil, name="")
53
+ key = ( root_key.nil? ? OpenSSL::PKey::RSA.new(2048) : root_key )
54
+ options = { :serial => 2, :subject => "/DC=org/DC=ruby-lang/CN=Ruby#{name}", :key => key, :issuer => root_ca.subject}
55
+ cert = new_certificate(options)
56
+ add_ca_extensions(cert, nil, root_ca)
57
+ [ cert.sign(key, OpenSSL::Digest::SHA256.new), key ]
58
+ end
59
+
60
+ def build_root_ca
61
+ key = OpenSSL::PKey::RSA.new(2048)
62
+ options = { :serial => 1, :subject => "/DC=org/DC=ruby-lang/CN=Ruby CA", :key => key}
63
+ ca = new_certificate(options)
64
+ add_ca_extensions(ca)
65
+ [ ca.sign(key, OpenSSL::Digest::SHA256.new), key ]
66
+ end
67
+
68
+ def new_certificate(options)
69
+ cert = OpenSSL::X509::Certificate.new
70
+ cert.version = 2
71
+ cert.serial = options.fetch(:serial, 1)
72
+ cert.subject = OpenSSL::X509::Name.parse(options.fetch(:subject, "/DC=org/DC=ruby-lang/CN=Ruby CA"))
73
+ cert.issuer = options.fetch(:issuer, cert.subject)
74
+ cert.public_key = options[:key].public_key
75
+ cert.not_before = Time.now
76
+ cert.not_after = cert.not_before + 2 * 365 * 86400
77
+ cert
78
+ end
79
+
80
+ def add_ca_extensions(certificate, subject=nil, issuer=nil)
81
+ factory = OpenSSL::X509::ExtensionFactory.new
82
+ factory.subject_certificate = (subject.nil? ? certificate : subject)
83
+ factory.issuer_certificate = (issuer.nil? ? certificate : issuer)
84
+
85
+ certificate.add_extension(factory.create_extension("basicConstraints","CA:TRUE",true))
86
+ certificate.add_extension(factory.create_extension("keyUsage","keyCertSign, cRLSign, digitalSignature", true))
87
+ certificate.add_extension(factory.create_extension("subjectKeyIdentifier","hash",false))
88
+ certificate.add_extension(factory.create_extension("authorityKeyIdentifier","keyid:always",false))
89
+ end
90
+
22
91
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-input-tcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.5
4
+ version: 3.0.0
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-25 00:00:00.000000000 Z
11
+ date: 2015-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logstash-core
@@ -100,6 +100,34 @@ dependencies:
100
100
  version: '0'
101
101
  prerelease: false
102
102
  type: :development
103
+ - !ruby/object:Gem::Dependency
104
+ name: flores
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 0.0.6
110
+ requirement: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ~>
113
+ - !ruby/object:Gem::Version
114
+ version: 0.0.6
115
+ prerelease: false
116
+ type: :development
117
+ - !ruby/object:Gem::Dependency
118
+ name: stud
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ~>
122
+ - !ruby/object:Gem::Version
123
+ version: 0.0.22
124
+ requirement: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ~>
127
+ - !ruby/object:Gem::Version
128
+ version: 0.0.22
129
+ prerelease: false
130
+ type: :development
103
131
  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
104
132
  email: info@elastic.co
105
133
  executables: []
@@ -138,10 +166,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
166
  version: '0'
139
167
  requirements: []
140
168
  rubyforge_project:
141
- rubygems_version: 2.4.8
169
+ rubygems_version: 2.4.6
142
170
  signing_key:
143
171
  specification_version: 4
144
172
  summary: Read events over a TCP socket.
145
173
  test_files:
146
174
  - spec/inputs/tcp_spec.rb
147
175
  - spec/spec_helper.rb
176
+ has_rdoc: