fluent-plugin-secure-forward 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ <source>
2
+ type secure_forward
3
+ self_hostname server
4
+ shared_key hogeposxxx0
5
+ cert_auto_generate yes
6
+ </source>
7
+
8
+ <match test.**>
9
+ type stdout
10
+ </match>
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.name = "fluent-plugin-secure-forward"
4
+ gem.version = "0.0.1"
5
+ gem.authors = ["TAGOMORI Satoshi"]
6
+ gem.email = ["tagomoris@gmail.com"]
7
+ gem.summary = %q{Fluentd input/output plugin to forward over SSL with authentications}
8
+ gem.description = %q{This version is HIGHLY EXPERIMENTAL. DON'T USE IN PRODUCTION}
9
+ gem.homepage = "https://github.com/tagomoris/fluent-plugin-secure-forward"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.require_paths = ["lib"]
15
+
16
+ gem.add_development_dependency "fluentd"
17
+ gem.add_development_dependency "fluent-mixin-config-placeholders"
18
+ gem.add_runtime_dependency "fluentd"
19
+ gem.add_runtime_dependency "fluent-mixin-config-placeholders"
20
+ end
@@ -0,0 +1,402 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'fluent/mixin/config_placeholders'
4
+
5
+ module Fluent
6
+ class SecureForwardInput < Input
7
+ DEFAULT_SECURE_LISTEN_PORT = 24284
8
+
9
+ Fluent::Plugin.register_input('secure_forward', self)
10
+
11
+ config_param :self_hostname, :string
12
+ include Fluent::Mixin::ConfigPlaceholders
13
+
14
+ config_param :shared_key, :string
15
+
16
+ config_param :bind, :string, :default => '0.0.0.0'
17
+ config_param :port, :integer, :default => DEFAULT_SECURE_LISTEN_PORT
18
+ config_param :allow_keepalive, :bool, :default => true #TODO: implement
19
+
20
+ config_param :allow_anonymous_source, :bool, :default => true
21
+ config_param :authentication, :bool, :default => false
22
+
23
+ ## meaningless for security...? not implemented yet
24
+ # config_param :dns_reverse_lookup_check, :bool, :default => false
25
+
26
+ config_param :cert_auto_generate, :bool, :default => false
27
+ config_param :generate_private_key_length, :integer, :default => 2048
28
+
29
+ config_param :generate_cert_country, :string, :default => 'US'
30
+ config_param :generate_cert_state, :string, :default => 'CA'
31
+ config_param :generate_cert_locality, :string, :default => 'Mountain View'
32
+ config_param :generate_cert_common_name, :string, :default => nil
33
+
34
+ config_param :cert_file_path, :string, :default => nil
35
+ config_param :private_key_file, :string, :default => nil
36
+ config_param :private_key_passphrase, :string, :default => nil
37
+
38
+ config_param :read_length, :size, :default => 8*1024*1024 # 8MB
39
+ config_param :read_interval_msec, :integer, :default => 50 # 50ms
40
+ config_param :socket_interval_msec, :integer, :default => 200 # 200ms
41
+
42
+ attr_reader :read_interval, :socket_interval
43
+
44
+ attr_reader :users # list of (username, password) by <user> tag
45
+ # <user>
46
+ # username ....
47
+ # password ....
48
+ # </user>
49
+ attr_reader :nodes # list of hosts, allowed to connect <server> tag (it includes source ip, shared_key(optional))
50
+ # <client>
51
+ # host ipaddr/hostname
52
+ # shared_key .... # optional shared key
53
+ # users username,list,of,allowed
54
+ # </client>
55
+
56
+ attr_reader :sessions # node/socket/thread list which has sslsocket instance keepaliving to client
57
+
58
+ def initialize
59
+ super
60
+ require 'resolv'
61
+ require 'socket'
62
+ require 'openssl'
63
+ require 'digest'
64
+ end
65
+
66
+ def configure(conf)
67
+ super
68
+
69
+ unless @cert_auto_generate || @cert_file_path
70
+ raise Fluent::ConfigError, "One of 'cert_auto_generate' or 'cert_file_path' must be specified"
71
+ end
72
+
73
+ @read_interval = @read_interval_msec / 1000.0
74
+ @socket_interval = @socket_interval_msec / 1000.0
75
+
76
+ @users = []
77
+ @nodes = []
78
+ conf.elements.each do |element|
79
+ case element.name
80
+ when 'user'
81
+ unless element['username'] && element['password']
82
+ raise Fluent::ConfigError, "username/password pair missing in <user>"
83
+ end
84
+ @users.push({
85
+ username: element['username'],
86
+ password: element['password']
87
+ })
88
+ when 'client'
89
+ unless element['host']
90
+ raise Fluent::ConfigError, "host missing in <client>"
91
+ end
92
+ @nodes.push({
93
+ host: element['host'],
94
+ shared_key: (element['shared_key'] || @shared_key),
95
+ users: (element['users'] ? element['users'].split(',') : nil),
96
+ })
97
+ else
98
+ raise Fluent::ConfigError, "unknown config tag name"
99
+ end
100
+ end
101
+
102
+ @generate_cert_common_name ||= @self_hostname
103
+ self.certificate
104
+ true
105
+ end
106
+
107
+ def start
108
+ super
109
+ OpenSSL::Random.seed(File.read("/dev/random", 16))
110
+ @sessions = []
111
+ @sock = nil
112
+ @listener = Thread.new(&method(:run))
113
+ end
114
+
115
+ def shutdown
116
+ @listener.kill
117
+ @listener.join
118
+ @sessions.each{ |s| s.shutdown }
119
+ @sock.close
120
+ end
121
+
122
+ def select_authenticate_users(node, username)
123
+ if node.nil? || node[:users].nil?
124
+ @users.select{|u| u[:username] == username}
125
+ else
126
+ @users.select{|u| node[:users].include?(u[:username]) && u[:username] == username}
127
+ end
128
+ end
129
+
130
+ def certificate
131
+ return @cert, @key if @cert && @key
132
+
133
+ if @cert_auto_generate
134
+ key = OpenSSL::PKey::RSA.generate(@generate_private_key_length)
135
+
136
+ digest = OpenSSL::Digest::SHA1.new
137
+ issuer = subject = OpenSSL::X509::Name.new
138
+ subject.add_entry('C', @generate_cert_country)
139
+ subject.add_entry('ST', @generate_cert_state)
140
+ subject.add_entry('L', @generate_cert_locality)
141
+ subject.add_entry('CN', @generate_cert_common_name)
142
+
143
+ cer = OpenSSL::X509::Certificate.new
144
+ cer.not_before = Time.at(0)
145
+ cer.not_after = Time.at(0)
146
+ cer.public_key = key
147
+ cer.serial = 1
148
+ cer.issuer = issuer
149
+ cer.subject = subject
150
+ cer.sign(key, digest)
151
+
152
+ @cert = cer
153
+ @key = key
154
+ return @cert, @key
155
+ end
156
+
157
+ @cert = OpenSSL::X509::Certificate.new(File.read(@cert_file_path))
158
+ @key = OpenSSL::PKey::RSA.new(File.read(@private_key_file), @private_key_passphrase)
159
+ end
160
+
161
+ def run # sslsocket server thread
162
+ cert, key = self.certificate
163
+ ctx = OpenSSL::SSL::SSLContext.new
164
+ ctx.cert = cert
165
+ ctx.key = key
166
+
167
+ server = TCPServer.new(@bind, @port)
168
+ @sock = OpenSSL::SSL::SSLServer.new(server, ctx)
169
+ loop do
170
+ while socket = @sock.accept
171
+ @sessions.push Session.new(self, socket)
172
+ end
173
+ end
174
+ end
175
+
176
+ def on_message(msg)
177
+ # NOTE: copy&paste from Fluent::ForwardInput#on_message(msg)
178
+
179
+ # TODO: format error
180
+ tag = msg[0].to_s
181
+ entries = msg[1]
182
+
183
+ if entries.class == String
184
+ # PackedForward
185
+ es = MessagePackEventStream.new(entries, @cached_unpacker)
186
+ Fluent::Engine.emit_stream(tag, es)
187
+
188
+ elsif entries.class == Array
189
+ # Forward
190
+ es = Fluent::MultiEventStream.new
191
+ entries.each {|e|
192
+ time = e[0].to_i
193
+ time = (now ||= Fluent::Engine.now) if time == 0
194
+ record = e[1]
195
+ es.add(time, record)
196
+ }
197
+ Fluent::Engine.emit_stream(tag, es)
198
+
199
+ else
200
+ # Message
201
+ time = msg[1]
202
+ time = Fluent::Engine.now if time == 0
203
+ record = msg[2]
204
+ Fluent::Engine.emit(tag, time, record)
205
+ end
206
+ end
207
+
208
+ class Session # Fluent::SecureForwardInput::Session
209
+ attr_accessor :receiver
210
+ attr_accessor :state, :thread, :node, :socket, :unpacker, :auth_salt
211
+
212
+ def initialize(receiver, socket)
213
+ @receiver = receiver
214
+
215
+ @state = :helo
216
+
217
+ @socket = socket
218
+ @socket.sync = true
219
+
220
+ @ipaddress = nil
221
+ @node = nil
222
+ @unpacker = MessagePack::Unpacker.new
223
+ @thread = Thread.new(&method(:start))
224
+ end
225
+
226
+ def established?
227
+ @state == :established
228
+ end
229
+
230
+ def generate_salt
231
+ OpenSSL::Random.random_bytes(16)
232
+ end
233
+
234
+ def check_node(hostname, ipaddress, port, proto)
235
+ node = nil
236
+ family = Socket.const_get(proto)
237
+ @receiver.nodes.each do |n|
238
+ proto, port, host, ipaddr, family_num, socktype_num, proto_num = Socket.getaddrinfo(n[:host], port, family).first
239
+ if ipaddr == ipaddress
240
+ node = n
241
+ break
242
+ end
243
+ end
244
+ node
245
+ end
246
+
247
+ ## not implemented yet
248
+ # def check_hostname_reverse_lookup(ipaddress)
249
+ # rev_name = Resolv.getname(ipaddress)
250
+ # proto, port, host, ipaddr, family_num, socktype_num, proto_num = Socket.getaddrinfo(rev_name, DUMMY_PORT)
251
+ # unless ipaddr == ipaddress
252
+ # return false
253
+ # end
254
+ # true
255
+ # end
256
+
257
+ def generate_helo
258
+ $log.debug "generating helo"
259
+ # ['HELO', options(hash)]
260
+ [ 'HELO', {'auth' => (@receiver.authentication ? @auth_key_salt : ''), 'keepalive' => @receiver.allow_keepalive } ]
261
+ end
262
+
263
+ def check_ping(message)
264
+ $log.debug "checking ping"
265
+ # ['PING', self_hostname, shared_key\_salt, sha512\_hex(shared_key\_salt + self_hostname + shared_key),
266
+ # username || '', sha512\_hex(auth\_salt + username + password) || '']
267
+ unless message.size == 6 && message[0] == 'PING'
268
+ return false, 'invalid ping message'
269
+ end
270
+ ping, hostname, shared_key_salt, shared_key_hexdigest, username, password_digest = message
271
+
272
+ shared_key = if @node && @node[:shared_key]
273
+ @node[:shared_key]
274
+ else
275
+ @receiver.shared_key
276
+ end
277
+ serverside = Digest::SHA512.new.update(shared_key_salt).update(hostname).update(shared_key).hexdigest
278
+ if shared_key_hexdigest != serverside
279
+ $log.warn "Shared key mismatch from '#{hostname}'"
280
+ return false, 'shared_key mismatch'
281
+ end
282
+
283
+ if @receiver.authentication
284
+ users = @receiver.select_authenticate_users(@node, username)
285
+ success = false
286
+ users.each do |user|
287
+ passhash = Digest::SHA512.new.update(@auth_key_salt).update(username).update(user[:password]).hexdigest
288
+ success ||= (passhash == password_digest)
289
+ end
290
+ unless success
291
+ $log.warn "Authentication failed from client '#{hostname}', username '#{username}'"
292
+ return false, 'username/password mismatch'
293
+ end
294
+ end
295
+
296
+ return true, shared_key_salt
297
+ end
298
+
299
+ def generate_pong(auth_result, reason_or_salt)
300
+ $log.debug "generating pong"
301
+ # ['PONG', bool(authentication result), 'reason if authentication failed',
302
+ # self_hostname, sha512\_hex(salt + self_hostname + sharedkey)]
303
+ if not auth_result
304
+ return ['PONG', false, reason_or_salt, '', '']
305
+ end
306
+
307
+ shared_key = if @node && @node[:shared_key]
308
+ @node[:shared_key]
309
+ else
310
+ @receiver.shared_key
311
+ end
312
+ shared_key_hex = Digest::SHA512.new.update(reason_or_salt).update(@receiver.self_hostname).update(shared_key).hexdigest
313
+ [ 'PONG', true, '', @receiver.self_hostname, shared_key_hex ]
314
+ end
315
+
316
+ def on_read(data)
317
+ $log.debug "on_read"
318
+ if self.established?
319
+ @receiver.on_message(data)
320
+ end
321
+
322
+ case @state
323
+ when :pingpong
324
+ success, reason_or_salt = self.check_ping(data)
325
+ if not success
326
+ send_data generate_pong(false, reason_or_salt)
327
+ self.shutdown
328
+ return
329
+ end
330
+ send_data generate_pong(true, reason_or_salt)
331
+
332
+ $log.debug "connection established"
333
+ @state = :established
334
+ end
335
+ end
336
+
337
+ def send_data(data)
338
+ # not nonblock because write data (response) needs sequence
339
+ @socket.write data.to_msgpack
340
+ end
341
+
342
+ def start
343
+ $log.debug "starting server"
344
+
345
+ proto, port, host, ipaddr = @socket.io.addr
346
+ @node = check_node(host, ipaddr, port, proto)
347
+ if @node.nil? && (! @receiver.allow_anonymous_source)
348
+ $log.warn "Connection required from unknown host '#{host}' (#{ipaddr}), disconnecting..."
349
+ self.shutdown
350
+ end
351
+
352
+ @auth_key_salt = generate_salt
353
+
354
+ buf = ''
355
+ read_length = @receiver.read_length
356
+ read_interval = @receiver.read_interval
357
+ socket_interval = @receiver.socket_interval
358
+
359
+ send_data generate_helo()
360
+ @state = :pingpong
361
+
362
+ loop do
363
+ begin
364
+ while @socket.read_nonblock(read_length, buf)
365
+ if buf == ''
366
+ sleep read_interval
367
+ next
368
+ end
369
+ @unpacker.feed_each(buf, &method(:on_read))
370
+ buf = ''
371
+ end
372
+ rescue OpenSSL::SSL::SSLError => e
373
+ # to wait i/o restart
374
+ sleep socket_interval
375
+ rescue EOFError => e
376
+ $log.debug "Connection closed from '#{host}'(#{ipaddr})"
377
+ break
378
+ end
379
+ end
380
+ self.shutdown
381
+ rescue => e
382
+ $log.warn e
383
+ end
384
+
385
+ def shutdown
386
+ @state = :closed
387
+ if @thread == Thread.current
388
+ @socket.close
389
+ @thread.kill
390
+ else
391
+ if @thread
392
+ @thread.kill
393
+ @thread.join
394
+ end
395
+ @socket.close
396
+ end
397
+ rescue => e
398
+ $log.debug "#{e.class}:#{e.message}"
399
+ end
400
+ end
401
+ end
402
+ end
@@ -0,0 +1,417 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'fluent/mixin/config_placeholders'
4
+
5
+ module Fluent
6
+ class SecureForwardOutput < ObjectBufferedOutput
7
+ DEFAULT_SECURE_CONNECT_PORT = 24284
8
+
9
+ Fluent::Plugin.register_output('secure_forward', self)
10
+
11
+ config_param :self_hostname, :string
12
+ include Fluent::Mixin::ConfigPlaceholders
13
+
14
+ config_param :shared_key, :string
15
+
16
+ # config_param :keepalive, :time, :default => 3600 # 0 means disable keepalive
17
+
18
+ config_param :send_timeout, :time, :default => 60
19
+ # config_param :hard_timeout, :time, :default => 60
20
+ # config_param :expire_dns_cache, :time, :default => 0 # 0 means disable cache
21
+
22
+ config_param :allow_self_signed_certificate, :bool, :default => true
23
+ config_param :ca_file_path, :string, :default => nil
24
+
25
+ config_param :read_length, :size, :default => 512 # 512bytes
26
+ config_param :read_interval_msec, :integer, :default => 50 # 50ms
27
+ config_param :socket_interval_msec, :integer, :default => 200 # 200ms
28
+
29
+ config_param :reconnect_interval, :time, :default => 15
30
+
31
+ attr_reader :read_interval, :socket_interval
32
+
33
+ attr_reader :nodes
34
+ # <server>
35
+ # host ipaddr/hostname
36
+ # hostlabel labelname # certification common name
37
+ # port 24284
38
+ # shared_key .... # optional shared key
39
+ # username name # if required
40
+ # password pass # if required
41
+ # </server>
42
+
43
+ def initialize
44
+ super
45
+ require 'socket'
46
+ require 'openssl'
47
+ require 'digest'
48
+ end
49
+
50
+ def configure(conf)
51
+ super
52
+
53
+ unless @allow_self_signed_certificate
54
+ raise Fluent::ConfigError, "not tested yet!"
55
+ end
56
+
57
+ @read_interval = @read_interval_msec / 1000.0
58
+ @socket_interval = @socket_interval_msec / 1000.0
59
+
60
+ # read <server> tags and set to nodes
61
+ @nodes = []
62
+ conf.elements.each do |element|
63
+ case element.name
64
+ when 'server'
65
+ unless element['host']
66
+ raise Fluent::ConfigError, "host missing in <server>"
67
+ end
68
+ node_shared_key = element['shared_key'] || @shared_key
69
+ @nodes.push Node.new(self, node_shared_key, element)
70
+ else
71
+ raise Fluent::ConfigError, "unknown config tag name #{element.name}"
72
+ end
73
+ end
74
+ if @nodes.size > 1
75
+ raise Fluent::ConfigError, "Two or more servers are not supported yet."
76
+ end
77
+
78
+ true
79
+ end
80
+
81
+ def select_node
82
+ #TODO: roundrobin? random?
83
+ @nodes.select(&:established?).first
84
+ end
85
+
86
+ def start
87
+ super
88
+
89
+ OpenSSL::Random.seed(File.read("/dev/random", 16))
90
+ @nodes.each do |node|
91
+ node.start
92
+ end
93
+ @nodewatcher = Thread.new(&method(:node_watcher))
94
+ end
95
+
96
+ def node_watcher
97
+ loop do
98
+ sleep @reconnect_interval
99
+ $log.debug "in node health watcher"
100
+ (0...(@nodes.size)).each do |i|
101
+ $log.debug "node health watcher for #{@nodes[i].host}"
102
+ if @nodes[i].state != :established
103
+ $log.info "dead connection found: #{@nodes[i].host}, reconnecting..."
104
+ node = @nodes[i]
105
+ @nodes[i] = node.dup
106
+ @nodes[i].start
107
+ node.shutdown
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def shutdown
114
+ @nodewatcher.kill
115
+ @nodewatcher.join
116
+ @nodes.each do |node|
117
+ node.shutdown
118
+ end
119
+ end
120
+
121
+ def write_objects(tag, es)
122
+ #TODO: check errors
123
+ node = select_node
124
+ unless node
125
+ raise "no one nodes with valid ssl session"
126
+ end
127
+
128
+ begin
129
+ send_data(node, tag, es)
130
+ rescue IOError => e
131
+ $log.warn "Failed to send messages to #{node.host}, parging."
132
+ node.shutdown
133
+ end
134
+ end
135
+
136
+ # MessagePack FixArray length = 2
137
+ FORWARD_HEADER = [0x92].pack('C')
138
+
139
+ # to forward messages
140
+ def send_data(node, tag, es)
141
+ ssl = node.sslsession
142
+ # beginArray(2)
143
+ ssl.write FORWARD_HEADER
144
+
145
+ # writeRaw(tag)
146
+ ssl.write tag.to_msgpack
147
+
148
+ # beginRaw(size)
149
+ sz = es.size
150
+ # # FixRaw
151
+ # ssl.write [0xa0 | sz].pack('C')
152
+ #elsif sz < 65536
153
+ # # raw 16
154
+ # ssl.write [0xda, sz].pack('Cn')
155
+ #else
156
+ # raw 32
157
+ ssl.write [0xdb, sz].pack('CN')
158
+ #end
159
+
160
+ # writeRawBody(packed_es)
161
+ es.write_to(ssl)
162
+ end
163
+
164
+ class Node # Fluent::SecureForwardOutput::Node
165
+ attr_accessor :host, :port, :hostlabel, :shared_key, :username, :password
166
+ attr_accessor :authentication, :keepalive
167
+ attr_accessor :socket, :sslsession, :unpacker, :shared_key_salt, :state
168
+
169
+ def initialize(sender, shared_key, conf)
170
+ @sender = sender
171
+ @shared_key = shared_key
172
+
173
+ @host = conf['host']
174
+ @port = (conf['port'] || DEFAULT_SECURE_CONNECT_PORT).to_i
175
+ @hostlabel = conf['hostlabel'] || conf['host']
176
+ @username = conf['username'] || ''
177
+ @password = conf['password'] || ''
178
+
179
+ @authentication = nil
180
+ @keepalive = nil
181
+
182
+ @socket = nil
183
+ @sslsession = nil
184
+ @unpacker = MessagePack::Unpacker.new
185
+
186
+ @shared_key_salt = generate_salt
187
+ @state = :helo
188
+ @thread = nil
189
+ end
190
+
191
+ def dup
192
+ Node.new(
193
+ @sender,
194
+ @shared_key,
195
+ {'host' => @host, 'port' => @port, 'hostlabel' => @hostlabel, 'username' => @username, 'password' => @password}
196
+ )
197
+ end
198
+
199
+ def start
200
+ @thread = Thread.new(&method(:connect))
201
+ end
202
+
203
+ def shutdown
204
+ $log.debug "shutting down node #{@host}"
205
+ @state = :closed
206
+
207
+ if @thread == Thread.current
208
+ @sslsession.close if @sslsession
209
+ @socket.close if @socket
210
+ @thread.kill
211
+ else
212
+ if @thread
213
+ @thread.kill
214
+ @thread.join
215
+ end
216
+ @sslsession.close if @sslsession
217
+ @socket.close if @socket
218
+ end
219
+ rescue => e
220
+ $log.debug "#{e.class}:#{e.message}"
221
+ end
222
+
223
+ def verify_result_name(code)
224
+ case code
225
+ when OpenSSL::X509::V_OK then 'V_OK'
226
+ when OpenSSL::X509::V_ERR_AKID_SKID_MISMATCH then 'V_ERR_AKID_SKID_MISMATCH'
227
+ when OpenSSL::X509::V_ERR_APPLICATION_VERIFICATION then 'V_ERR_APPLICATION_VERIFICATION'
228
+ when OpenSSL::X509::V_ERR_CERT_CHAIN_TOO_LONG then 'V_ERR_CERT_CHAIN_TOO_LONG'
229
+ when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED then 'V_ERR_CERT_HAS_EXPIRED'
230
+ when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID then 'V_ERR_CERT_NOT_YET_VALID'
231
+ when OpenSSL::X509::V_ERR_CERT_REJECTED then 'V_ERR_CERT_REJECTED'
232
+ when OpenSSL::X509::V_ERR_CERT_REVOKED then 'V_ERR_CERT_REVOKED'
233
+ when OpenSSL::X509::V_ERR_CERT_SIGNATURE_FAILURE then 'V_ERR_CERT_SIGNATURE_FAILURE'
234
+ when OpenSSL::X509::V_ERR_CERT_UNTRUSTED then 'V_ERR_CERT_UNTRUSTED'
235
+ when OpenSSL::X509::V_ERR_CRL_HAS_EXPIRED then 'V_ERR_CRL_HAS_EXPIRED'
236
+ when OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID then 'V_ERR_CRL_NOT_YET_VALID'
237
+ when OpenSSL::X509::V_ERR_CRL_SIGNATURE_FAILURE then 'V_ERR_CRL_SIGNATURE_FAILURE'
238
+ when OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT then 'V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT'
239
+ when OpenSSL::X509::V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD then 'V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD'
240
+ when OpenSSL::X509::V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD then 'V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD'
241
+ when OpenSSL::X509::V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD then 'V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD'
242
+ when OpenSSL::X509::V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD then 'V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD'
243
+ when OpenSSL::X509::V_ERR_INVALID_CA then 'V_ERR_INVALID_CA'
244
+ when OpenSSL::X509::V_ERR_INVALID_PURPOSE then 'V_ERR_INVALID_PURPOSE'
245
+ when OpenSSL::X509::V_ERR_KEYUSAGE_NO_CERTSIGN then 'V_ERR_KEYUSAGE_NO_CERTSIGN'
246
+ when OpenSSL::X509::V_ERR_OUT_OF_MEM then 'V_ERR_OUT_OF_MEM'
247
+ when OpenSSL::X509::V_ERR_PATH_LENGTH_EXCEEDED then 'V_ERR_PATH_LENGTH_EXCEEDED'
248
+ when OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN then 'V_ERR_SELF_SIGNED_CERT_IN_CHAIN'
249
+ when OpenSSL::X509::V_ERR_SUBJECT_ISSUER_MISMATCH then 'V_ERR_SUBJECT_ISSUER_MISMATCH'
250
+ when OpenSSL::X509::V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY then 'V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY'
251
+ when OpenSSL::X509::V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE then 'V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY'
252
+ when OpenSSL::X509::V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE then 'V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE'
253
+ when OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL then 'V_ERR_UNABLE_TO_GET_CRL'
254
+ when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT then 'V_ERR_UNABLE_TO_GET_ISSUER_CERT'
255
+ when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY then 'V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
256
+ when OpenSSL::X509::V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE then 'V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE'
257
+ end
258
+ end
259
+
260
+ def established?
261
+ @state == :established
262
+ end
263
+
264
+ def generate_salt
265
+ OpenSSL::Random.random_bytes(16)
266
+ end
267
+
268
+ def check_helo(message)
269
+ $log.debug "checking helo"
270
+ # ['HELO', options(hash)]
271
+ unless message.size == 2 && message[0] == 'HELO'
272
+ return false
273
+ end
274
+ opts = message[1]
275
+ @authentication = opts['auth']
276
+ @keepalive = opts['keepalive']
277
+ true
278
+ end
279
+
280
+ def generate_ping
281
+ $log.debug "generating ping"
282
+ # ['PING', self_hostname, sharedkey\_salt, sha512\_hex(sharedkey\_salt + self_hostname + shared_key),
283
+ # username || '', sha512\_hex(auth\_salt + username + password) || '']
284
+ shared_key_hexdigest = Digest::SHA512.new.update(@shared_key_salt).update(@sender.self_hostname).update(@shared_key).hexdigest
285
+ ping = ['PING', @sender.self_hostname, @shared_key_salt, shared_key_hexdigest]
286
+ if @authentication != ''
287
+ password_hexdigest = Digest::SHA512.new.update(@authentication).update(@username).update(@password).hexdigest
288
+ ping.push(@username, password_hexdigest)
289
+ else
290
+ ping.push('','')
291
+ end
292
+ ping
293
+ end
294
+
295
+ def check_pong(message)
296
+ $log.debug "checking pong"
297
+ # ['PONG', bool(authentication result), 'reason if authentication failed',
298
+ # self_hostname, sha512\_hex(salt + self_hostname + sharedkey)]
299
+ unless message.size == 5 && message[0] == 'PONG'
300
+ return false, 'invalid format for PONG message'
301
+ end
302
+ pong, auth_result, reason, hostname, shared_key_hexdigest = message
303
+
304
+ unless auth_result
305
+ return false, 'authentication failed: ' + reason
306
+ end
307
+
308
+ clientside = Digest::SHA512.new.update(@shared_key_salt).update(hostname).update(@shared_key).hexdigest
309
+ unless shared_key_hexdigest == clientside
310
+ return false, 'shared key mismatch'
311
+ end
312
+
313
+ return true, nil
314
+ end
315
+
316
+ def send_data(data)
317
+ @sslsession.write data.to_msgpack
318
+ end
319
+
320
+ def on_read(data)
321
+ $log.debug "on_read"
322
+ if self.established?
323
+ #TODO: ACK
324
+ $log.warn "unknown packets arrived..."
325
+ return
326
+ end
327
+
328
+ case @state
329
+ when :helo
330
+ # TODO: log debug
331
+ unless check_helo(data)
332
+ $log.warn "received invalid helo message from #{@host}"
333
+ self.shutdown
334
+ return
335
+ end
336
+ send_data generate_ping()
337
+ @state = :pingpong
338
+ when :pingpong
339
+ success, reason = check_pong(data)
340
+ unless success
341
+ $log.warn "connection refused to #{@host}:" + reason
342
+ self.shutdown
343
+ return
344
+ end
345
+ $log.info "connection established to #{@host}"
346
+ @state = :established
347
+ end
348
+ end
349
+
350
+ def connect
351
+ $log.debug "starting client"
352
+ sock = TCPSocket.new(@host, @port)
353
+
354
+ opt = [1, @sender.send_timeout.to_i].pack('I!I!') # { int l_onoff; int l_linger; }
355
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_LINGER, opt)
356
+
357
+ opt = [@sender.send_timeout.to_i, 0].pack('L!L!') # struct timeval
358
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, opt)
359
+
360
+ # TODO: SSLContext constructer parameter (SSL/TLS protocol version)
361
+ context = OpenSSL::SSL::SSLContext.new
362
+ context.ca_file = @cert_file_path
363
+ # TODO: context.ciphers= (SSL Shared key chiper protocols)
364
+
365
+ sslsession = OpenSSL::SSL::SSLSocket.new(sock, context)
366
+ sslsession.connect
367
+
368
+ begin
369
+ unless @sender.allow_self_signed_certificate
370
+ $log.debug sslsession.peer_cert.subject.to_s
371
+ sslsession.post_connection_check(@hostlabel)
372
+ verify = sslsession.verify_result
373
+ if verify != OpenSSL::X509::V_OK
374
+ err_name = verify_result_name(verify)
375
+ $log.warn "failed to verify certification while connecting host #{@host} as #{@hostlabel} (but not raised, why?)"
376
+ $log.warn "verify_result: #{err_name}"
377
+ raise RuntimeError, "failed to verify certification while connecting host #{@host} as #{@hostlabel}"
378
+ end
379
+ end
380
+ rescue OpenSSL::SSL::SSLError => e
381
+ $log.warn "failed to verify certification while connecting host #{@host} as #{@hostlabel}"
382
+ self.shutdown
383
+ raise
384
+ end
385
+
386
+ $log.debug "ssl sessison connected"
387
+ @socket = sock
388
+ @sslsession = sslsession
389
+
390
+ buf = ''
391
+ read_length = @sender.read_length
392
+ read_interval = @sender.read_interval
393
+ socket_interval = @sender.socket_interval
394
+
395
+ loop do
396
+ begin
397
+ while @sslsession.read_nonblock(read_length, buf)
398
+ if buf == ''
399
+ sleep read_interval
400
+ next
401
+ end
402
+ @unpacker.feed_each(buf, &method(:on_read))
403
+ buf = ''
404
+ end
405
+ rescue OpenSSL::SSL::SSLError
406
+ # to wait i/o restart
407
+ sleep socket_interval
408
+ rescue EOFError
409
+ $log.warn "disconnected from #{@host}"
410
+ break
411
+ end
412
+ end
413
+ self.shutdown
414
+ end
415
+ end
416
+ end
417
+ end