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.
- checksums.yaml +7 -0
- data/lib/log-courier/client.rb +352 -0
- data/lib/log-courier/client_tls.rb +217 -0
- data/lib/log-courier/event_queue.rb +194 -0
- data/lib/log-courier/server.rb +185 -0
- data/lib/log-courier/server_tcp.rb +275 -0
- data/lib/log-courier/server_zmq.rb +219 -0
- metadata +78 -0
@@ -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
|