log-courier 1.0.21.ga82ca4c

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,194 @@
1
+ # encoding: utf-8
2
+
3
+ # Copyright 2014 Jason Woods.
4
+ #
5
+ # This file is a modification of code from Ruby.
6
+ # Ruby is copyrighted free software by Yukihiro Matsumoto <matz@netlab.jp>.
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ #
21
+ # This is a queue implementation dervied from SizedQueue, but with a timeout.
22
+ #
23
+ # It is significantly faster than using SizedQueue wrapped in Timeout::timeout
24
+ # because it uses mutex.sleep, whereas Timeout::timeout actually starts another
25
+ # thread that waits and then raises exception or has to be stopped on exiting
26
+ # the block.
27
+ #
28
+ # The majority of the code is taken from Ruby's SizedQueue<Queue implementation.
29
+ #
30
+ module LogCourier
31
+ class EventQueue
32
+ #
33
+ # Creates a fixed-length queue with a maximum size of +max+.
34
+ #
35
+ def initialize(max)
36
+ raise ArgumentError, "queue size must be positive" unless max > 0
37
+ @max = max
38
+ @enque_cond = ConditionVariable.new
39
+ @num_enqueue_waiting = 0
40
+
41
+ @que = []
42
+ @que.taint # enable tainted communication
43
+ @num_waiting = 0
44
+ self.taint
45
+ @mutex = Mutex.new
46
+ @cond = ConditionVariable.new
47
+ end
48
+
49
+ #
50
+ # Returns the maximum size of the queue.
51
+ #
52
+ def max
53
+ @max
54
+ end
55
+
56
+ #
57
+ # Sets the maximum size of the queue.
58
+ #
59
+ def max=(max)
60
+ raise ArgumentError, "queue size must be positive" unless max > 0
61
+
62
+ @mutex.synchronize do
63
+ if max <= @max
64
+ @max = max
65
+ else
66
+ diff = max - @max
67
+ @max = max
68
+ diff.times do
69
+ @enque_cond.signal
70
+ end
71
+ end
72
+ end
73
+ max
74
+ end
75
+
76
+ #
77
+ # Pushes +obj+ to the queue. If there is no space left in the queue, waits
78
+ # until space becomes available, up to a maximum of +timeout+ seconds.
79
+ #
80
+ def push(obj, timeout = nil)
81
+ unless timeout.nil?
82
+ start = Time.now
83
+ end
84
+ @mutex.synchronize do
85
+ loop do
86
+ break if @que.length < @max
87
+ @num_enqueue_waiting += 1
88
+ begin
89
+ @enque_cond.wait @mutex, timeout
90
+ ensure
91
+ @num_enqueue_waiting -= 1
92
+ end
93
+ raise TimeoutError if !timeout.nil? and Time.now - start >= timeout
94
+ end
95
+
96
+ @que.push obj
97
+ @cond.signal
98
+ end
99
+ self
100
+ end
101
+
102
+ #
103
+ # Alias of push
104
+ #
105
+ alias << push
106
+
107
+ #
108
+ # Alias of push
109
+ #
110
+ alias enq push
111
+
112
+ #
113
+ # Retrieves data from the queue and runs a waiting thread, if any.
114
+ #
115
+ def pop(*args)
116
+ retval = _pop_timeout *args
117
+ @mutex.synchronize do
118
+ if @que.length < @max
119
+ @enque_cond.signal
120
+ end
121
+ end
122
+ retval
123
+ end
124
+
125
+ #
126
+ # Retrieves data from the queue. If the queue is empty, the calling thread is
127
+ # suspended until data is pushed onto the queue or, if set, +timeout+ seconds
128
+ # passes. If +timeout+ is 0, the thread isn't suspended, and an exception is
129
+ # raised.
130
+ #
131
+ def _pop_timeout(timeout = nil)
132
+ unless timeout.nil?
133
+ start = Time.now
134
+ end
135
+ @mutex.synchronize do
136
+ loop do
137
+ return @que.shift unless @que.empty?
138
+ raise TimeoutError if timeout == 0
139
+ begin
140
+ @num_waiting += 1
141
+ @cond.wait @mutex, timeout
142
+ ensure
143
+ @num_waiting -= 1
144
+ end
145
+ raise TimeoutError if !timeout.nil? and Time.now - start >= timeout
146
+ end
147
+ end
148
+ end
149
+
150
+ #
151
+ # Alias of pop
152
+ #
153
+ alias shift pop
154
+
155
+ #
156
+ # Alias of pop
157
+ #
158
+ alias deq pop
159
+
160
+ #
161
+ # Returns +true+ if the queue is empty.
162
+ #
163
+ def empty?
164
+ @que.empty?
165
+ end
166
+
167
+ #
168
+ # Removes all objects from the queue.
169
+ #
170
+ def clear
171
+ @que.clear
172
+ self
173
+ end
174
+
175
+ #
176
+ # Returns the length of the queue.
177
+ #
178
+ def length
179
+ @que.length
180
+ end
181
+
182
+ #
183
+ # Alias of length.
184
+ #
185
+ alias size length
186
+
187
+ #
188
+ # Returns the number of threads waiting on the queue.
189
+ #
190
+ def num_waiting
191
+ @num_waiting + @num_enqueue_waiting
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,185 @@
1
+ # encoding: utf-8
2
+
3
+ # Copyright 2014 Jason Woods.
4
+ #
5
+ # This file is a modification of code from Logstash Forwarder.
6
+ # Copyright 2012-2013 Jordan Sissel and contributors.
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ require 'log-courier/event_queue'
21
+ require 'multi_json'
22
+ require 'thread'
23
+ require 'zlib'
24
+
25
+ module LogCourier
26
+ class TimeoutError < StandardError; end
27
+ class ShutdownSignal < StandardError; end
28
+ class ProtocolError < StandardError; end
29
+
30
+ # Implementation of the server
31
+ class Server
32
+ attr_reader :port
33
+
34
+ def initialize(options = {})
35
+ @options = {
36
+ logger: nil,
37
+ transport: 'tls'
38
+ }.merge!(options)
39
+
40
+ @logger = @options[:logger]
41
+
42
+ case @options[:transport]
43
+ when 'tcp', 'tls'
44
+ require 'log-courier/server_tcp'
45
+ @server = ServerTcp.new(@options)
46
+ when 'plainzmq', 'zmq'
47
+ require 'log-courier/server_zmq'
48
+ @server = ServerZmq.new(@options)
49
+ else
50
+ raise '[LogCourierServer] \'transport\' must be tcp, tls, plainzmq or zmq'
51
+ end
52
+
53
+ # Grab the port back
54
+ @port = @server.port
55
+
56
+ # Load the json adapter
57
+ @json_adapter = MultiJson.adapter.instance
58
+ @json_options = { raw: true, use_bigdecimal: true }
59
+ end
60
+
61
+ def run(&block)
62
+ # TODO: Make queue size configurable
63
+ event_queue = EventQueue.new 1
64
+ server_thread = nil
65
+
66
+ begin
67
+ server_thread = Thread.new do
68
+ # Receive messages and process them
69
+ @server.run do |signature, message, comm|
70
+ case signature
71
+ when 'PING'
72
+ process_ping message, comm
73
+ when 'JDAT'
74
+ process_jdat message, comm, event_queue
75
+ else
76
+ @logger.warn("[LogCourierServer] Unknown message received from #{comm.peer}") unless @logger.nil?
77
+ # Don't kill a client that sends a bad message
78
+ # Just reject it and let it send it again, potentially to another server
79
+ comm.send '????', ''
80
+ end
81
+ end
82
+ end
83
+
84
+ loop do
85
+ block.call event_queue.pop
86
+ end
87
+ ensure
88
+ # Signal the server thread to stop
89
+ unless server_thread.nil?
90
+ server_thread.raise ShutdownSignal
91
+ server_thread.join
92
+ end
93
+ end
94
+ end
95
+
96
+ def process_ping(message, comm)
97
+ # Size of message should be 0
98
+ if message.length != 0
99
+ raise ProtocolError, "unexpected data attached to ping message (#{message.length})"
100
+ end
101
+
102
+ # PONG!
103
+ # NOTE: comm.send can raise a Timeout::Error of its own
104
+ comm.send 'PONG', ''
105
+ end
106
+
107
+ def process_jdat(message, comm, event_queue)
108
+ # Now we have the data, aim to respond within 5 seconds
109
+ ack_timeout = Time.now.to_i + 5
110
+
111
+ # OK - first is a nonce - we send this back with sequence acks
112
+ # This allows the client to know what is being acknowledged
113
+ # Nonce is 16 so check we have enough
114
+ if message.length < 17
115
+ raise ProtocolError, "JDAT message too small (#{message.length})"
116
+ end
117
+
118
+ nonce = message[0...16]
119
+
120
+ # The remainder of the message is the compressed data block
121
+ message = StringIO.new Zlib::Inflate.inflate(message[16...message.length])
122
+
123
+ # Message now contains JSON encoded events
124
+ # They are aligned as [length][event]... so on
125
+ # We acknowledge them by their 1-index position in the stream
126
+ # A 0 sequence acknowledgement means we haven't processed any yet
127
+ sequence = 0
128
+ events = []
129
+ length_buf = ''
130
+ data_buf = ''
131
+ loop do
132
+ ret = message.read 4, length_buf
133
+ if ret.nil?
134
+ # Finished!
135
+ break
136
+ elsif length_buf.length < 4
137
+ raise ProtocolError, "JDAT length extraction failed (#{ret} #{length_buf.length})"
138
+ end
139
+
140
+ length = length_buf.unpack('N').first
141
+
142
+ # Extract message
143
+ ret = message.read length, data_buf
144
+ if ret.nil? or data_buf.length < length
145
+ @logger.warn()
146
+ raise ProtocolError, "JDAT message extraction failed #{ret} #{data_buf.length}"
147
+ end
148
+
149
+ data_buf.force_encoding('utf-8')
150
+
151
+ # Ensure valid encoding
152
+ unless data_buf.valid_encoding?
153
+ data_buf.chars.map do |c|
154
+ c.valid_encoding? ? c : "\xEF\xBF\xBD"
155
+ end
156
+ end
157
+
158
+ # Decode the JSON
159
+ begin
160
+ event = @json_adapter.load(data_buf, @json_options)
161
+ rescue MultiJson::ParserError => e
162
+ @logger.warn("[LogCourierServer] JSON parse failure, falling back to plain-text: #{e}") unless @logger.nil?
163
+ event = { 'message' => data_buf }
164
+ end
165
+
166
+ # Queue the event
167
+ begin
168
+ event_queue.push event, [0, ack_timeout - Time.now.to_i].max
169
+ rescue TimeoutError
170
+ # Full pipeline, partial ack
171
+ # NOTE: comm.send can raise a Timeout::Error of its own
172
+ comm.send 'ACKN', [nonce, sequence].pack('A*N')
173
+ ack_timeout = Time.now.to_i + 5
174
+ retry
175
+ end
176
+
177
+ sequence += 1
178
+ end
179
+
180
+ # Acknowledge the full message
181
+ # NOTE: comm.send can raise a Timeout::Error
182
+ comm.send 'ACKN', [nonce, sequence].pack('A*N')
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,275 @@
1
+ # encoding: utf-8
2
+
3
+ # Copyright 2014 Jason Woods.
4
+ #
5
+ # This file is a modification of code from Logstash Forwarder.
6
+ # Copyright 2012-2013 Jordan Sissel and contributors.
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ require 'openssl'
21
+ require 'socket'
22
+ require 'thread'
23
+
24
+ module LogCourier
25
+ # Wrap around TCPServer to grab last error for use in reporting which peer had an error
26
+ class ExtendedTCPServer < TCPServer
27
+ # Yield the peer
28
+ def accept
29
+ sock = super
30
+ peer = sock.peeraddr(:numeric)
31
+ Thread.current['LogCourierPeer'] = "#{peer[2]}:#{peer[1]}"
32
+ return sock
33
+ end
34
+ end
35
+
36
+ # TLS transport implementation for server
37
+ class ServerTcp
38
+ attr_reader :port
39
+
40
+ # Create a new TLS transport endpoint
41
+ def initialize(options = {})
42
+ @options = {
43
+ logger: nil,
44
+ transport: 'tls',
45
+ port: 0,
46
+ address: '0.0.0.0',
47
+ ssl_certificate: nil,
48
+ ssl_key: nil,
49
+ ssl_key_passphrase: nil,
50
+ ssl_verify: false,
51
+ ssl_verify_default_ca: false,
52
+ ssl_verify_ca: nil,
53
+ max_packet_size: 10_485_760,
54
+ }.merge!(options)
55
+
56
+ @logger = @options[:logger]
57
+
58
+ if @options[:transport] == 'tls'
59
+ [:ssl_certificate, :ssl_key].each do |k|
60
+ raise "[LogCourierServer] '#{k}' is required" if @options[k].nil?
61
+ end
62
+
63
+ if @options[:ssl_verify] and (!@options[:ssl_verify_default_ca] && @options[:ssl_verify_ca].nil?)
64
+ raise '[LogCourierServer] Either \'ssl_verify_default_ca\' or \'ssl_verify_ca\' must be specified when ssl_verify is true'
65
+ end
66
+ end
67
+
68
+ begin
69
+ @tcp_server = ExtendedTCPServer.new(@options[:address], @options[:port])
70
+
71
+ # Query the port in case the port number is '0'
72
+ # TCPServer#addr == [ address_family, port, address, address ]
73
+ @port = @tcp_server.addr[1]
74
+
75
+ if @options[:transport] == 'tls'
76
+ ssl = OpenSSL::SSL::SSLContext.new
77
+ ssl.cert = OpenSSL::X509::Certificate.new(File.read(@options[:ssl_certificate]))
78
+ ssl.key = OpenSSL::PKey::RSA.new(File.read(@options[:ssl_key]), @options[:ssl_key_passphrase])
79
+
80
+ if @options[:ssl_verify]
81
+ cert_store = OpenSSL::X509::Store.new
82
+
83
+ # Load the system default certificate path to the store
84
+ cert_store.set_default_paths if @options[:ssl_verify_default_ca]
85
+
86
+ if File.directory?(@options[:ssl_verify_ca])
87
+ cert_store.add_path(@options[:ssl_verify_ca])
88
+ else
89
+ cert_store.add_file(@options[:ssl_verify_ca])
90
+ end
91
+
92
+ ssl.cert_store = cert_store
93
+
94
+ ssl.verify_mode = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
95
+ end
96
+
97
+ @server = OpenSSL::SSL::SSLServer.new(@tcp_server, ssl)
98
+ else
99
+ @server = @tcp_server
100
+ end
101
+
102
+ if @options[:port] == 0
103
+ @logger.warn '[LogCourierServer] Transport ' + @options[:transport] + ' is listening on ephemeral port ' + @port.to_s
104
+ end
105
+ rescue => e
106
+ raise "[LogCourierServer] Failed to initialise: #{e}"
107
+ end
108
+ end # def initialize
109
+
110
+ def run(&block)
111
+ client_threads = {}
112
+
113
+ loop do
114
+ # This means ssl accepting is single-threaded.
115
+ begin
116
+ client = @server.accept
117
+ rescue EOFError, OpenSSL::SSL::SSLError, IOError => e
118
+ # Handshake failure or other issue
119
+ peer = Thread.current['LogCourierPeer'] || 'unknown'
120
+ @logger.warn "[LogCourierServer] Connection from #{peer} failed to initialise: #{e}" unless @logger.nil?
121
+ client.close rescue nil
122
+ next
123
+ end
124
+
125
+ peer = Thread.current['LogCourierPeer'] || 'unknown'
126
+
127
+ @logger.info "[LogCourierServer] New connection from #{peer}" unless @logger.nil?
128
+
129
+ # Clear up finished threads
130
+ client_threads.delete_if do |_, thr|
131
+ !thr.alive?
132
+ end
133
+
134
+ # Start a new connection thread
135
+ client_threads[client] = Thread.new(client, peer) do |client_copy, peer_copy|
136
+ ConnectionTcp.new(@logger, client_copy, peer_copy, @options).run(&block)
137
+ end
138
+ end
139
+ rescue ShutdownSignal
140
+ # Capture shutting down signal
141
+ 0
142
+ ensure
143
+ # Raise shutdown in all client threads and join then
144
+ client_threads.each do |_, thr|
145
+ thr.raise ShutdownSignal
146
+ end
147
+
148
+ client_threads.each(&:join)
149
+
150
+ @tcp_server.close
151
+ end
152
+ end
153
+
154
+ # Representation of a single connected client
155
+ class ConnectionTcp
156
+ attr_accessor :peer
157
+
158
+ def initialize(logger, fd, peer, options)
159
+ @logger = logger
160
+ @fd = fd
161
+ @peer = peer
162
+ @in_progress = false
163
+ @options = options
164
+ end
165
+
166
+ def run
167
+ loop do
168
+ # Read messages
169
+ # Each message begins with a header
170
+ # 4 byte signature
171
+ # 4 byte length
172
+ # Normally we would not parse this inside transport, but for TLS we have to in order to locate frame boundaries
173
+ signature, length = recv(8).unpack('A4N')
174
+
175
+ # Sanity
176
+ if length > @options[:max_packet_size]
177
+ raise ProtocolError, "packet too large (#{length} > #{@options[:max_packet_size]})"
178
+ end
179
+
180
+ # While we're processing, EOF is bad as it may occur during send
181
+ @in_progress = true
182
+
183
+ # Read the message
184
+ if length == 0
185
+ data = ''
186
+ else
187
+ data = recv(length)
188
+ end
189
+
190
+ # Send for processing
191
+ yield signature, data, self
192
+
193
+ # If we EOF next it's a graceful close
194
+ @in_progress = false
195
+ end
196
+ rescue TimeoutError
197
+ # Timeout of the connection, we were idle too long without a ping/pong
198
+ @logger.warn("[LogCourierServer] Connection from #{@peer} timed out") unless @logger.nil?
199
+ rescue EOFError
200
+ if @in_progress
201
+ @logger.warn("[LogCourierServer] Premature connection close on connection from #{@peer}") unless @logger.nil?
202
+ else
203
+ @logger.info("[LogCourierServer] Connection from #{@peer} closed") unless @logger.nil?
204
+ end
205
+ rescue OpenSSL::SSL::SSLError, IOError, Errno::ECONNRESET => e
206
+ # Read errors, only action is to shutdown which we'll do in ensure
207
+ @logger.warn("[LogCourierServer] SSL error on connection from #{@peer}: #{e}") unless @logger.nil?
208
+ rescue ProtocolError => e
209
+ # Connection abort request due to a protocol error
210
+ @logger.warn("[LogCourierServer] Protocol error on connection from #{@peer}: #{e}") unless @logger.nil?
211
+ rescue ShutdownSignal
212
+ # Shutting down
213
+ @logger.warn("[LogCourierServer] Closing connecting from #{@peer}: server shutting down") unless @logger.nil?
214
+ rescue => e
215
+ # Some other unknown problem
216
+ @logger.warn("[LogCourierServer] Unknown error on connection from #{@peer}: #{e}") unless @logger.nil?
217
+ @logger.warn("[LogCourierServer] #{e.backtrace}: #{e.message} (#{e.class})") unless @logger.nil?
218
+ ensure
219
+ @fd.close rescue nil
220
+ end
221
+
222
+ def recv(need)
223
+ reset_timeout
224
+ have = ''
225
+ loop do
226
+ begin
227
+ buffer = @fd.read_nonblock need - have.length
228
+ rescue IO::WaitReadable
229
+ raise TimeoutError if IO.select([@fd], nil, [@fd], @timeout - Time.now.to_i).nil?
230
+ retry
231
+ rescue IO::WaitWritable
232
+ raise TimeoutError if IO.select(nil, [@fd], [@fd], @timeout - Time.now.to_i).nil?
233
+ retry
234
+ end
235
+ if buffer.nil?
236
+ raise EOFError
237
+ elsif buffer.length == 0
238
+ raise ProtocolError, "read failure (#{have.length}/#{need})"
239
+ end
240
+ if have.length == 0
241
+ have = buffer
242
+ else
243
+ have << buffer
244
+ end
245
+ break if have.length >= need
246
+ end
247
+ have
248
+ end
249
+
250
+ def send(signature, message)
251
+ reset_timeout
252
+ data = signature + [message.length].pack('N') + message
253
+ done = 0
254
+ loop do
255
+ begin
256
+ written = @fd.write_nonblock(data[done...data.length])
257
+ rescue IO::WaitReadable
258
+ raise TimeoutError if IO.select([@fd], nil, [@fd], @timeout - Time.now.to_i).nil?
259
+ retry
260
+ rescue IO::WaitWritable
261
+ raise TimeoutError if IO.select(nil, [@fd], [@fd], @timeout - Time.now.to_i).nil?
262
+ retry
263
+ end
264
+ raise ProtocolError, "write failure (#{done}/#{data.length})" if written == 0
265
+ done += written
266
+ break if done >= data.length
267
+ end
268
+ end
269
+
270
+ def reset_timeout
271
+ # TODO: Make configurable
272
+ @timeout = Time.now.to_i + 1_800
273
+ end
274
+ end
275
+ end