logstash-input-tcp 4.2.2-java

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,55 @@
1
+ # encoding: utf-8
2
+ require 'java'
3
+
4
+ java_import org.logstash.tcp.Decoder
5
+
6
+ class DecoderImpl
7
+
8
+ include Decoder
9
+
10
+ def initialize(codec, tcp)
11
+ @tcp = tcp
12
+ @codec = codec
13
+ @first_read = true
14
+ end
15
+
16
+ def decode(channel_addr, data)
17
+ bytes = Java::byte[data.readableBytes].new
18
+ data.getBytes(0, bytes)
19
+ data.release
20
+ tbuf = String.from_java_bytes bytes, "ASCII-8BIT"
21
+ if @first_read
22
+ tbuf = init_first_read(channel_addr, tbuf)
23
+ end
24
+ @tcp.decode_buffer(@address, @port, @codec,
25
+ @proxy_address, @proxy_port, tbuf)
26
+ end
27
+
28
+ def copy()
29
+ DecoderImpl.new(@codec.clone, @tcp)
30
+ end
31
+
32
+ private
33
+ def init_first_read(channel_addr, received)
34
+ if @tcp.proxy_protocol
35
+ pp_hdr, filtered = received.split("\r\n", 2)
36
+ pp_info = pp_hdr.split(/\s/)
37
+ # PROXY proto clientip proxyip clientport proxyport
38
+ if pp_info[0] != "PROXY"
39
+ @tcp.logger.error("invalid proxy protocol header label", :hdr => pp_hdr)
40
+ raise IOError
41
+ else
42
+ @proxy_address = pp_info[3]
43
+ @proxy_port = pp_info[5]
44
+ @address = pp_info[2]
45
+ @port = pp_info[4]
46
+ end
47
+ else
48
+ filtered = received
49
+ @address = channel_addr.get_address.get_host_address
50
+ @port = channel_addr.get_port
51
+ end
52
+ @first_read = false
53
+ filtered
54
+ end
55
+ end
@@ -0,0 +1,36 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'logstash-input-tcp'
3
+ s.version = ::File.read('version').split("\n").first
4
+ s.licenses = ['Apache License (2.0)']
5
+ s.summary = "Read events over a TCP socket."
6
+ s.description = "This gem is a Logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install gemname. This gem is not a stand-alone program"
7
+ s.authors = ["Elastic"]
8
+ s.email = 'info@elastic.co'
9
+ s.homepage = "http://www.elastic.co/guide/en/logstash/current/index.html"
10
+ s.platform = "java"
11
+ s.require_paths = ["lib", "vendor/jar-dependencies"]
12
+
13
+ # Files
14
+ s.files = Dir["lib/**/*","spec/**/*","*.gemspec","*.md","CONTRIBUTORS","Gemfile","LICENSE","NOTICE.TXT", "vendor/jar-dependencies/**/*.jar", "vendor/jar-dependencies/**/*.rb", "version", "docs/**/*"]
15
+
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "logstash_group" => "input" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency "logstash-core-plugin-api", ">= 1.60", "<= 2.99"
24
+
25
+ # line vs streaming codecs required for fix_streaming_codecs
26
+ # TODO: fix_streaming_codecs should be refactored to not
27
+ # require the codecs to be installed.
28
+ s.add_runtime_dependency 'logstash-codec-plain'
29
+ s.add_runtime_dependency 'logstash-codec-line'
30
+ s.add_runtime_dependency 'logstash-codec-json'
31
+ s.add_runtime_dependency 'logstash-codec-json_lines'
32
+
33
+ s.add_development_dependency 'logstash-devutils'
34
+ s.add_development_dependency 'flores', '~> 0.0.6'
35
+ s.add_development_dependency 'stud', '~> 0.0.22'
36
+ end
@@ -0,0 +1,421 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "socket"
4
+ require "timeout"
5
+ require "logstash/json"
6
+ require "logstash/inputs/tcp"
7
+ require "stud/try"
8
+ require "stud/task"
9
+ require "flores/pki"
10
+ require "openssl"
11
+
12
+ require_relative "../spec_helper"
13
+
14
+ #Cabin::Channel.get(LogStash).subscribe(STDOUT)
15
+ #Cabin::Channel.get(LogStash).level = :debug
16
+ describe LogStash::Inputs::Tcp do
17
+
18
+ context "codec (PR #1372)" do
19
+ it "switches from plain to line" do
20
+ require "logstash/codecs/plain"
21
+ require "logstash/codecs/line"
22
+ plugin = LogStash::Inputs::Tcp.new("codec" => LogStash::Codecs::Plain.new, "port" => 0)
23
+ plugin.register
24
+ insist { plugin.codec }.is_a?(LogStash::Codecs::Line)
25
+ plugin.close
26
+ end
27
+ it "switches from json to json_lines" do
28
+ require "logstash/codecs/json"
29
+ require "logstash/codecs/json_lines"
30
+ plugin = LogStash::Inputs::Tcp.new("codec" => LogStash::Codecs::JSON.new, "port" => 0)
31
+ plugin.register
32
+ insist { plugin.codec }.is_a?(LogStash::Codecs::JSONLines)
33
+ plugin.close
34
+ end
35
+ end
36
+
37
+ it "should read plain with unicode" do
38
+ event_count = 10
39
+ port = rand(1024..65535)
40
+ conf = <<-CONFIG
41
+ input {
42
+ tcp {
43
+ port => #{port}
44
+ }
45
+ }
46
+ CONFIG
47
+
48
+ host = '127.0.0.1'
49
+ events = input(conf) do |pipeline, queue|
50
+ socket = Stud::try(5.times) { TCPSocket.new(host, port) }
51
+ event_count.times do |i|
52
+ # unicode smiley for testing unicode support!
53
+ socket.puts("#{i} ☹")
54
+ socket.flush
55
+ end
56
+ socket.close
57
+
58
+ event_count.times.collect {queue.pop}
59
+ end
60
+
61
+ insist { events.length } == event_count
62
+ event_count.times do |i|
63
+ event = events[i]
64
+ insist { event.get("message") } == "#{i} ☹"
65
+ insist { event.get("host") } == host
66
+ end
67
+ end
68
+
69
+ it "should handle PROXY protocol v1 connections" do
70
+ event_count = 10
71
+ port = rand(1024..65535)
72
+ conf = <<-CONFIG
73
+ input {
74
+ tcp {
75
+ port => #{port}
76
+ proxy_protocol => true
77
+ }
78
+ }
79
+ CONFIG
80
+
81
+ events = input(conf) do |pipeline, queue|
82
+ socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
83
+ socket.puts("PROXY TCP4 1.2.3.4 5.6.7.8 1234 5678\r");
84
+ socket.flush
85
+ event_count.times do |i|
86
+ # unicode smiley for testing unicode support!
87
+ socket.puts("#{i} ☹")
88
+ socket.flush
89
+ end
90
+ socket.close
91
+
92
+ event_count.times.collect {queue.pop}
93
+ end
94
+
95
+ insist { events.length } == event_count
96
+ event_count.times do |i|
97
+ insist { events[i].get("message") } == "#{i} ☹"
98
+ insist { events[i].get("host") } == "1.2.3.4"
99
+ insist { events[i].get("port") } == "1234"
100
+ insist { events[i].get("proxy_host") } == "5.6.7.8"
101
+ insist { events[i].get("proxy_port") } == "5678"
102
+ end
103
+ end
104
+
105
+ it "should read events with plain codec and ISO-8859-1 charset" do
106
+ port = rand(1024..65535)
107
+ charset = "ISO-8859-1"
108
+ conf = <<-CONFIG
109
+ input {
110
+ tcp {
111
+ port => #{port}
112
+ codec => plain { charset => "#{charset}" }
113
+ }
114
+ }
115
+ CONFIG
116
+
117
+ event = input(conf) do |pipeline, queue|
118
+ socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
119
+ text = "\xA3" # the £ symbol in ISO-8859-1 aka Latin-1
120
+ text.force_encoding("ISO-8859-1")
121
+ socket.puts(text)
122
+ socket.close
123
+
124
+ queue.pop
125
+ end
126
+
127
+ # Make sure the 0xA3 latin-1 code converts correctly to UTF-8.
128
+ insist { event.get("message").size } == 1
129
+ insist { event.get("message").bytesize } == 2
130
+ insist { event.get("message") } == "£"
131
+ end
132
+
133
+ it "should read events with json codec" do
134
+ port = rand(1024..65535)
135
+ conf = <<-CONFIG
136
+ input {
137
+ tcp {
138
+ port => #{port}
139
+ codec => json
140
+ }
141
+ }
142
+ CONFIG
143
+
144
+ data = {
145
+ "hello" => "world",
146
+ "foo" => [1,2,3],
147
+ "baz" => { "1" => "2" },
148
+ "host" => "example host"
149
+ }
150
+
151
+ event = input(conf) do |pipeline, queue|
152
+ socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
153
+ socket.puts(LogStash::Json.dump(data))
154
+ socket.close
155
+
156
+ queue.pop
157
+ end
158
+
159
+ insist { event.get("hello") } == data["hello"]
160
+ insist { event.get("foo").to_a } == data["foo"] # to_a to cast Java ArrayList produced by JrJackson
161
+ insist { event.get("baz") } == data["baz"]
162
+
163
+ # Make sure the tcp input, w/ json codec, uses the event's 'host' value,
164
+ # if present, instead of providing its own
165
+ insist { event.get("host") } == data["host"]
166
+ end
167
+
168
+ it "should read events with json codec (testing 'host' handling)" do
169
+ port = rand(1024..65535)
170
+ conf = <<-CONFIG
171
+ input {
172
+ tcp {
173
+ port => #{port}
174
+ codec => json
175
+ }
176
+ }
177
+ CONFIG
178
+
179
+ data = {
180
+ "hello" => "world"
181
+ }
182
+
183
+ event = input(conf) do |pipeline, queue|
184
+ socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
185
+ socket.puts(LogStash::Json.dump(data))
186
+ socket.close
187
+
188
+ queue.pop
189
+ end
190
+
191
+ insist { event.get("hello") } == data["hello"]
192
+ insist { event }.include?("host")
193
+ end
194
+
195
+ it "should read events with json_lines codec" do
196
+ port = rand(1024..65535)
197
+ conf = <<-CONFIG
198
+ input {
199
+ tcp {
200
+ port => #{port}
201
+ codec => json_lines
202
+ }
203
+ }
204
+ CONFIG
205
+
206
+ data = {
207
+ "hello" => "world",
208
+ "foo" => [1,2,3],
209
+ "baz" => { "1" => "2" },
210
+ "idx" => 0
211
+ }
212
+ event_count = 5
213
+
214
+ events = input(conf) do |pipeline, queue|
215
+ socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
216
+ (1..event_count).each do |idx|
217
+ data["idx"] = idx
218
+ socket.puts(LogStash::Json.dump(data) + "\n")
219
+ end
220
+ socket.close
221
+
222
+ (1..event_count).map{queue.pop}
223
+ end
224
+
225
+ events.each_with_index do |event, idx|
226
+ insist { event.get("hello") } == data["hello"]
227
+ insist { event.get("foo").to_a } == data["foo"] # to_a to cast Java ArrayList produced by JrJackson
228
+ insist { event.get("baz") } == data["baz"]
229
+ insist { event.get("idx") } == idx + 1
230
+ end # do
231
+ end # describe
232
+
233
+ it "should one message per connection" do
234
+ event_count = 10
235
+ port = rand(1024..65535)
236
+ conf = <<-CONFIG
237
+ input {
238
+ tcp {
239
+ port => #{port}
240
+ }
241
+ }
242
+ CONFIG
243
+
244
+ events = input(conf) do |pipeline, queue|
245
+ event_count.times do |i|
246
+ socket = Stud::try(5.times) { TCPSocket.new("127.0.0.1", port) }
247
+ socket.puts("#{i}")
248
+ socket.flush
249
+ socket.close
250
+ end
251
+
252
+ # since each message is sent on its own tcp connection & thread, exact receiving order cannot be garanteed
253
+ event_count.times.collect{queue.pop}.sort_by{|event| event.get("message")}
254
+ end
255
+
256
+ event_count.times do |i|
257
+ insist { events[i].get("message") } == "#{i}"
258
+ end
259
+ end
260
+
261
+ # below are new specs added in the context of the shutdown semantic refactor.
262
+ # TODO:
263
+ # - refactor all specs using this new model
264
+ # - pipelineless_input has been basically copied from the udp input specs, it should be DRYied up
265
+ # - see if we should miminc the udp input UDPClient helper class instead of directly using TCPSocket
266
+
267
+ describe "LogStash::Inputs::Tcp new specs style" do
268
+
269
+ before do
270
+ srand(RSpec.configuration.seed)
271
+ end
272
+
273
+ let(:port) { rand(1024..65535) }
274
+ subject { LogStash::Plugin.lookup("input", "tcp").new({ "port" => port }) }
275
+ let!(:helper) { TcpHelpers.new }
276
+
277
+ after :each do
278
+ subject.close rescue nil
279
+ end
280
+
281
+ describe "#register" do
282
+ it "should register without errors" do
283
+ expect { subject.register }.to_not raise_error
284
+ end
285
+ end
286
+
287
+ describe "#receive" do
288
+ shared_examples "receiving events" do
289
+ # TODO(sissel): Implement normal event-receipt tests as as a shared example
290
+ end
291
+
292
+ context "when ssl_enable is true" do
293
+ let(:pki) { Flores::PKI.generate }
294
+ let(:certificate) { pki[0] }
295
+ let(:key) { pki[1] }
296
+ let(:certificate_file) { Stud::Temporary.file }
297
+ let(:key_file) { Stud::Temporary.file }
298
+ let(:queue) { Queue.new }
299
+
300
+ let(:config) do
301
+ {
302
+ "host" => "127.0.0.1",
303
+ "port" => port,
304
+ "ssl_enable" => true,
305
+ "ssl_cert" => certificate_file.path,
306
+ "ssl_key" => key_file.path,
307
+
308
+ # Trust our self-signed cert.
309
+ # TODO(sissel): Make this a separate certificate for the client
310
+ "ssl_extra_chain_certs" => certificate_file.path
311
+ }
312
+ end
313
+
314
+ subject(:input) { LogStash::Plugin.lookup("input", "tcp").new(config) }
315
+
316
+ before do
317
+ certificate_file.write(certificate)
318
+ key_file.write(key)
319
+
320
+ # Close to flush the file writes.
321
+ certificate_file.close
322
+ key_file.close
323
+ subject.register
324
+ end
325
+
326
+ after do
327
+ File.unlink(certificate_file.path)
328
+ File.unlink(key_file.path)
329
+ end
330
+
331
+ context "with a poorly-behaving client" do
332
+ let!(:input_task) { Stud::Task.new { input.run(queue) } }
333
+
334
+ after { input.close }
335
+
336
+ context "that disconnects before doing TLS handshake" do
337
+ before do
338
+ client = TCPSocket.new("127.0.0.1", port)
339
+ client.close
340
+ end
341
+
342
+ it "should not negatively impact the plugin" do
343
+ # TODO(sissel): Look for a better way to detect this failure
344
+ # besides a sleep/wait.
345
+ result = input_task.thread.join(0.5)
346
+ expect(result).to be_nil
347
+ end
348
+ end
349
+
350
+ context "that sends garbage instead of TLS handshake" do
351
+ let!(:input_task) { Stud::Task.new { input.run(queue) } }
352
+ let(:max_length) { 1000 }
353
+ let(:garbage) { Flores::Random.iterations(max_length).collect { Flores::Random.integer(1...255) }.pack("C*") }
354
+ before do
355
+ # Assertion to verify this test is actually sending something.
356
+ expect(garbage.length).to be > 0
357
+
358
+ client = TCPSocket.new("127.0.0.1", port)
359
+ client.write(garbage)
360
+ client.flush
361
+ Thread.new { sleep(1); client.close }
362
+ end
363
+ it "should not negatively impact the plugin" do
364
+ # TODO(sissel): Look for a better way to detect this failure besides a sleep/wait.
365
+ result = input_task.thread.join(0.5)
366
+ expect(result).to be_nil
367
+ end
368
+ end
369
+
370
+ context "connection was healthy but now has garbage or corruption" do
371
+ let!(:input_task) { Stud::Task.new { input.run(queue) } }
372
+ let(:tcp) { TCPSocket.new("127.0.0.1", port) }
373
+ let(:sslcontext) { OpenSSL::SSL::SSLContext.new }
374
+ let(:sslsocket) { OpenSSL::SSL::SSLSocket.new(tcp, sslcontext) }
375
+ let(:max_length) { 1000 }
376
+ let(:garbage) { Flores::Random.iterations(max_length).collect { Flores::Random.integer(1...255) }.pack("C*") }
377
+
378
+ before do
379
+ sslcontext.cert = certificate
380
+ sslcontext.key = key
381
+ sslcontext.verify_mode = OpenSSL::SSL::VERIFY_NONE
382
+
383
+ sslsocket.connect
384
+ sslsocket.write("Hello world\n")
385
+
386
+ # Assertion to verify this test is actually sending something.
387
+ expect(garbage.length).to be > 0
388
+ tcp.write(garbage)
389
+ tcp.flush
390
+ sslsocket.close
391
+ tcp.close
392
+ end
393
+
394
+ it "should not negatively impact the plugin" do
395
+ # TODO(sissel): Look for a better way to detect this failure besides a sleep/wait.
396
+ result = input_task.thread.join(0.5)
397
+ expect(result).to be_nil
398
+ end
399
+ end
400
+ end
401
+
402
+ # TODO(sissel): Spec multiple clients where only one is bad.
403
+
404
+ context "with client certificate problems" do
405
+ context "using an expired certificate"
406
+ context "using an untrusted certificate"
407
+ end
408
+
409
+ context "with a good connection" do
410
+ # TODO(sissel): use shared example
411
+ include_examples "receiving events"
412
+ end
413
+
414
+ end
415
+ end
416
+
417
+ it_behaves_like "an interruptible input plugin" do
418
+ let(:config) { { "port" => port } }
419
+ end
420
+ end
421
+ end