lib/gelf/transport/tcp_tls.rb
require 'openssl'
module GELF
module Transport
# Provides encryption capabilities for TCP connections
class TCPTLS < TCP
# Supported tls_options:
# 'no_default_ca' [Boolean] prevents OpenSSL from using the systems CA store.
# 'version' [Symbol] any of :TLSv1, :TLSv1_1, :TLSv1_2 (default)
# 'ca' [String] the path to a custom CA store
# 'cert' [String, IO] the client certificate file
# 'key' [String, IO] the key for the client certificate
# 'all_ciphers' [Boolean] allows any ciphers to be used, may be insecure
# 'rescue_ssl_errors' [Boolean] similar to rescue_network_errors in notifier.rb, allows SSL exceptions to be raised
# 'no_verify' [Boolean] disable peer verification
attr_accessor :rescue_ssl_errors
def initialize(addresses, tls_options={})
@tls_options = tls_options
@rescue_ssl_errors = @tls_options['rescue_ssl_errors']
@rescue_ssl_errors if @rescue_ssl_errors.nil?
super(addresses)
end
protected
def write_socket(socket, message)
super(socket, message)
rescue OpenSSL::SSL::SSLError
socket.close unless socket.closed?
raise unless rescue_ssl_errors
false
end
def connect(host, port)
plain_socket = super(host, port)
start_tls(plain_socket)
rescue OpenSSL::SSL::SSLError
plain_socket.close unless plain_socket.closed?
raise unless rescue_ssl_errors
nil
end
# Initiates TLS communication on the socket
def start_tls(plain_socket)
ssl_socket_class.new(plain_socket, ssl_context).tap do |ssl_socket|
ssl_socket.sync_close = true
ssl_socket.connect
end
end
def ssl_socket_class
if defined?(Celluloid::IO::SSLSocket)
Celluloid::IO::SSLSocket
else
OpenSSL::SSL::SSLSocket
end
end
def ssl_context
@ssl_context ||= OpenSSL::SSL::SSLContext.new.tap do |ctx|
ctx.cert_store = ssl_cert_store
ctx.ssl_version = tls_version
ctx.verify_mode = verify_mode
set_certificate_and_key(ctx)
restrict_ciphers(ctx) unless @tls_options['all_ciphers']
end
end
def set_certificate_and_key(context)
return unless @tls_options['cert'] && @tls_options['key']
context.cert = OpenSSL::X509::Certificate.new(resource(@tls_options['cert']))
context.key = OpenSSL::PKey::RSA.new(resource(@tls_options['key']))
end
# checks whether {resource} is a filename and tries to read it
# otherwise treats it as if it already contains certificate/key data
def resource(data)
if data.is_a?(String) && File.exist?(data)
File.read(data)
else
data
end
end
# Ciphers have to come from the CipherString class, specifically the _TXT_ constants here - https://github.com/jruby/jruby-openssl/blob/master/src/main/java/org/jruby/ext/openssl/CipherStrings.java#L47-L178
def restrict_ciphers(ctx)
# This CipherString is will allow a variety of 'currently' cryptographically secure ciphers,
# while also retaining a broad level of compatibility
ctx.ciphers = "TLSv1_2:TLSv1_1:TLSv1:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!ECDSA:!ADH:!IDEA:!3DES"
end
def verify_mode
@tls_options['no_verify'] ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
end
# SSL v2&3 are insecure, forces at least TLS v1.0 and defaults to v1.2
def tls_version
if @tls_options.key?('version') &&
OpenSSL::SSL::SSLContext::METHODS.include?(@tls_options['version']) &&
@tls_options['version'] =~ /\ATLSv/
@tls_options['version']
else
:TLSv1_2
end
end
def ssl_cert_store
OpenSSL::X509::Store.new.tap do |store|
unless @tls_options['no_default_ca']
store.set_default_paths
end
if @tls_options.key?('ca')
ca = @tls_options['ca']
if File.directory?(ca)
store.add_path(@tls_options['ca'])
elsif File.file?(ca)
store.add_file(ca)
else
$stderr.puts "No directory or file: #{ca}"
end
end
end
end
end
end
end