-
Notifications
You must be signed in to change notification settings - Fork 75
/
Copy pathtcp.rb
312 lines (258 loc) · 10 KB
/
tcp.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# encoding: utf-8
require "logstash/inputs/base"
require "logstash/util/socket_peer"
require "socket"
require "openssl"
# Read events over a TCP socket.
#
# Like stdin and file inputs, each event is assumed to be one line of text.
#
# Can either accept connections from clients or connect to a server,
# depending on `mode`.
class LogStash::Inputs::Tcp < LogStash::Inputs::Base
config_name "tcp"
default :codec, "line"
# When mode is `server`, the address to listen on.
# When mode is `client`, the address to connect to.
config :host, :validate => :string, :default => "0.0.0.0"
# When mode is `server`, the port to listen on.
# When mode is `client`, the port to connect to.
config :port, :validate => :number, :required => true
config :data_timeout, :validate => :number, :default => -1, :deprecated => "This setting is not used by this plugin. It will be removed soon."
# Mode to operate in. `server` listens for client connections,
# `client` connects to a server.
config :mode, :validate => ["server", "client"], :default => "server"
# Enable SSL (must be set for other `ssl_` options to take effect).
config :ssl_enable, :validate => :boolean, :default => false
# Verify the identity of the other end of the SSL connection against the CA.
# For input, sets the field `sslsubject` to that of the client certificate.
config :ssl_verify, :validate => :boolean, :default => true
# The SSL CA certificate, chainfile or CA path. The system CA path is automatically included.
config :ssl_cacert, :validate => :path, :deprecated => "This setting is deprecated in favor of ssl_extra_chain_certs as it sets a more clear expectation to add more X509 certificates to the store"
# SSL certificate path
config :ssl_cert, :validate => :path
# SSL key path
config :ssl_key, :validate => :path
# SSL key passphrase
config :ssl_key_passphrase, :validate => :password, :default => nil
# An Array of extra X509 certificates to be added to the certificate chain.
# Useful when the CA chain is not necessary in the system store.
config :ssl_extra_chain_certs, :validate => :array, :default => []
HOST_FIELD = "host".freeze
PORT_FIELD = "port".freeze
SSLSUBJECT_FIELD = "sslsubject".freeze
def initialize(*args)
super(*args)
# monkey patch TCPSocket and SSLSocket to include socket peer
TCPSocket.module_eval{include ::LogStash::Util::SocketPeer}
OpenSSL::SSL::SSLSocket.module_eval{include ::LogStash::Util::SocketPeer}
# threadsafe socket bookkeeping
@server_socket = nil
@client_socket = nil
@connection_sockets = {}
@socket_mutex = Mutex.new
@ssl_context = nil
end
def register
fix_streaming_codecs
# note that since we are opening a socket in register, we must also make sure we close it
# in the close method even if we also close it in the stop method since we could have
# a situation where register is called but not run & stop.
self.server_socket = new_server_socket if server?
end
def run(output_queue)
if server?
run_server(output_queue)
else
run_client(output_queue)
end
end
def stop
# force close all sockets which will escape any blocking read with a IO exception
# and any thread using them will exit.
# catch all rescue nil on close to discard any close errors or invalid socket
server_socket.close rescue nil
client_socket.close rescue nil
connection_sockets.each{|socket| socket.close rescue nil}
end
def close
# see related comment in register: we must make sure to close the server socket here
# because it is created in the register method and we could be in the context of having
# register called but never run & stop, only close.
# catch all rescue nil on close to discard any close errors or invalid socket
server_socket.close rescue nil
end
private
def run_server(output_queue)
while !stop?
begin
socket = add_connection_socket(server_socket.accept)
# start a new thread for each connection.
server_connection_thread(output_queue, socket)
rescue OpenSSL::SSL::SSLError => e
# log error, close socket, accept next connection
@logger.debug? && @logger.debug("SSL Error", :exception => e, :backtrace => e.backtrace)
rescue => e
# if this exception occured while the plugin is stopping
# just ignore and exit
raise e unless stop?
end
end
ensure
# catch all rescue nil on close to discard any close errors or invalid socket
server_socket.close rescue nil
end
def run_client(output_queue)
while !stop?
self.client_socket = new_client_socket
handle_socket(client_socket, client_socket.peeraddr[3], client_socket.peeraddr[1], output_queue, @codec.clone)
end
ensure
# catch all rescue nil on close to discard any close errors or invalid socket
client_socket.close rescue nil
end
def server_connection_thread(output_queue, socket)
Thread.new(output_queue, socket) do |q, s|
begin
@logger.debug? && @logger.debug("Accepted connection", :client => s.peer, :server => "#{@host}:#{@port}")
handle_socket(s, s.peeraddr[3], s.peeraddr[1], q, @codec.clone)
ensure
delete_connection_socket(s)
end
end
end
def handle_socket(socket, client_address, client_port, output_queue, codec)
peer = "#{client_address}:#{client_port}"
while !stop?
codec.decode(read(socket)) do |event|
event.set(HOST_FIELD, client_address) unless event.get(HOST_FIELD)
event.set(PORT_FIELD, client_port) unless event.get(PORT_FIELD)
event.set(SSLSUBJECT_FIELD, socket.peer_cert.subject.to_s) if @ssl_enable && @ssl_verify && event.get(SSLSUBJECT_FIELD).nil?
decorate(event)
output_queue << event
end
end
rescue EOFError
@logger.debug? && @logger.debug("Connection closed", :client => peer)
rescue Errno::ECONNRESET
@logger.debug? && @logger.debug("Connection reset by peer", :client => peer)
rescue OpenSSL::SSL::SSLError => e
# Fixes issue #23
@logger.error("SSL Error", :exception => e, :backtrace => e.backtrace)
socket.close rescue nil
rescue => e
# if plugin is stopping, don't bother logging it as an error
!stop? && @logger.error("An error occurred. Closing connection", :client => peer, :exception => e, :backtrace => e.backtrace)
ensure
# catch all rescue nil on close to discard any close errors or invalid socket
socket.close rescue nil
codec.respond_to?(:flush) && codec.flush do |event|
event.set(HOST_FIELD, client_address) unless event.get(HOST_FIELD)
event.set(PORT_FIELD, client_port) unless event.get(PORT_FIELD)
event.set(SSLSUBJECT_FIELD, socket.peer_cert.subject.to_s) if @ssl_enable && @ssl_verify && event.get(SSLSUBJECT_FIELD).nil?
decorate(event)
output_queue << event
end
end
private
def client_thread(output_queue, socket)
Thread.new(output_queue, socket) do |q, s|
begin
@logger.debug? && @logger.debug("Accepted connection", :client => s.peer, :server => "#{@host}:#{@port}")
handle_socket(s, s.peeraddr[3], s.peeraddr[1], q, @codec.clone)
rescue Interrupted
s.close rescue nil
ensure
@client_threads_lock.synchronize{@client_threads.delete(Thread.current)}
end
end
end
private
def server?
@mode == "server"
end
def read(socket)
socket.sysread(16384)
end
def ssl_context
return @ssl_context if @ssl_context
begin
@ssl_context = OpenSSL::SSL::SSLContext.new
@ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(@ssl_cert))
@ssl_context.key = OpenSSL::PKey::RSA.new(File.read(@ssl_key),@ssl_key_passphrase.value)
@ssl_context.cert_store = load_cert_store
if @ssl_verify
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
end
rescue => e
@logger.error("Could not inititalize SSL context", :exception => e, :backtrace => e.backtrace)
raise e
end
@ssl_context
end
def load_cert_store
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
if File.directory?(@ssl_cacert)
cert_store.add_path(@ssl_cacert)
else
cert_store.add_file(@ssl_cacert)
end if @ssl_cacert
@ssl_extra_chain_certs.each do |cert|
cert_store.add_file(cert)
end
cert_store
end
def new_server_socket
@logger.info("Starting tcp input listener", :address => "#{@host}:#{@port}")
begin
socket = TCPServer.new(@host, @port)
rescue Errno::EADDRINUSE
@logger.error("Could not start TCP server: Address in use", :host => @host, :port => @port)
raise
end
@ssl_enable ? OpenSSL::SSL::SSLServer.new(socket, ssl_context) : socket
end
def new_client_socket
socket = TCPSocket.new(@host, @port)
if @ssl_enable
socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
socket.connect
end
@logger.debug? && @logger.debug("Opened connection", :client => "#{socket.peer}")
socket
rescue OpenSSL::SSL::SSLError => e
@logger.error("SSL Error", :exception => e, :backtrace => e.backtrace)
# catch all rescue nil on close to discard any close errors or invalid socket
socket.close rescue nil
sleep(1) # prevent hammering peer
retry
rescue
# if this exception occured while the plugin is stopping
# just ignore and exit
raise unless stop?
end
# threadsafe sockets bookkeeping
def client_socket=(socket)
@socket_mutex.synchronize{@client_socket = socket}
end
def client_socket
@socket_mutex.synchronize{@client_socket}
end
def server_socket=(socket)
@socket_mutex.synchronize{@server_socket = socket}
end
def server_socket
@socket_mutex.synchronize{@server_socket}
end
def add_connection_socket(socket)
@socket_mutex.synchronize{@connection_sockets[socket] = true}
socket
end
def delete_connection_socket(socket)
@socket_mutex.synchronize{@connection_sockets.delete(socket)}
end
def connection_sockets
@socket_mutex.synchronize{@connection_sockets.keys.dup}
end
end