1919module SyslogTls
2020 # Supports SSL connection to remote host
2121 class SSLTransport
22+ CONNECT_TIMEOUT = 10
23+ # READ_TIMEOUT = 5
24+ WRITE_TIMEOUT = 5
25+
2226 attr_accessor :socket
2327
2428 attr_reader :host , :port , :client_cert , :client_key , :ssl_version
@@ -37,28 +41,74 @@ def initialize(host, port, client_cert: nil, client_key: nil, ssl_version: :TLSv
3741
3842 def connect
3943 @socket = get_ssl_connection
40- @socket . connect
44+ begin
45+ begin
46+ @socket . connect_nonblock
47+ rescue Errno ::EAGAIN , Errno ::EWOULDBLOCK , IO ::WaitReadable
48+ select_with_timeout ( @socket , :connect_read ) && retry
49+ rescue IO ::WaitWritable
50+ select_with_timeout ( @socket , :connect_write ) && retry
51+ end
52+ rescue Errno ::ETIMEDOUT
53+ raise 'Socket timeout during connect'
54+ end
55+ end
56+
57+ def get_tcp_connection
58+ tcp = nil
59+
60+ family = Socket ::Constants ::AF_UNSPEC
61+ sock_type = Socket ::Constants ::SOCK_STREAM
62+ addr_info = Socket . getaddrinfo ( host , port , family , sock_type , nil , nil , false ) . first
63+ _ , port , _ , address , family , sock_type = addr_info
64+
65+ begin
66+ sock_addr = Socket . sockaddr_in ( port , address )
67+ tcp = Socket . new ( family , sock_type , 0 )
68+ tcp . setsockopt ( Socket ::SOL_SOCKET , Socket ::Constants ::SO_REUSEADDR , true )
69+ tcp . setsockopt ( Socket ::SOL_SOCKET , Socket ::Constants ::SO_REUSEPORT , true )
70+ tcp . connect_nonblock ( sock_addr )
71+ rescue Errno ::EINPROGRESS
72+ select_with_timeout ( tcp , :connect_write )
73+ begin
74+ tcp . connect_nonblock ( sock_addr )
75+ rescue Errno ::EISCONN
76+ # all good
77+ rescue SystemCallError
78+ tcp . close rescue nil
79+ raise
80+ end
81+ rescue SystemCallError
82+ tcp . close rescue nil
83+ raise
84+ end
85+
86+ tcp . setsockopt ( Socket ::IPPROTO_TCP , Socket ::TCP_NODELAY , true )
87+ tcp
4188 end
4289
4390 def get_ssl_connection
44- tcp = TCPSocket . new ( host , port )
91+ tcp = get_tcp_connection
4592
4693 ctx = OpenSSL ::SSL ::SSLContext . new
47- ctx . set_params ( verify_mode : OpenSSL ::SSL ::VERIFY_PEER )
94+ ctx . verify_mode = OpenSSL ::SSL ::VERIFY_PEER
4895 ctx . ssl_version = ssl_version
4996
5097 ctx . cert = OpenSSL ::X509 ::Certificate . new ( File . read ( client_cert ) ) if client_cert
5198 ctx . key = OpenSSL ::PKey ::read ( File . read ( client_key ) ) if client_key
52- OpenSSL ::SSL ::SSLSocket . new ( tcp , ctx )
99+ socket = OpenSSL ::SSL ::SSLSocket . new ( tcp , ctx )
100+ socket . sync_close = true
101+ socket
53102 end
54103
55104 # Allow to retry on failed writes
56105 def write ( s )
57106 begin
58107 retry_id ||= 0
59- @socket . send ( :write , s )
108+ do_write ( s )
60109 rescue => e
61110 if ( retry_id += 1 ) < @retries
111+ @socket . close rescue nil
62112 connect
63113 retry
64114 else
@@ -67,6 +117,41 @@ def write(s)
67117 end
68118 end
69119
120+ def do_write ( data )
121+ data . force_encoding ( 'BINARY' ) # so we can break in the middle of multi-byte characters
122+ loop do
123+ sent = 0
124+ begin
125+ sent = @socket . write_nonblock ( data )
126+ rescue OpenSSL ::SSL ::SSLError , Errno ::EAGAIN , Errno ::EWOULDBLOCK , IO ::WaitWritable => e
127+ if e . is_a? ( OpenSSL ::SSL ::SSLError ) && e . message !~ /write would block/
128+ raise e
129+ else
130+ select_with_timeout ( @socket , :write ) && retry
131+ end
132+ end
133+
134+ break if sent >= data . size
135+ data = data [ sent , data . size ]
136+ end
137+ end
138+
139+ def select_with_timeout ( tcp , type )
140+ o = case type
141+ when :connect_read
142+ args = [ [ tcp ] , nil , nil , CONNECT_TIMEOUT ]
143+ when :connect_write
144+ args = [ nil , [ tcp ] , nil , CONNECT_TIMEOUT ]
145+ # when :read
146+ # args = [[tcp], nil, nil, READ_TIMEOUT]
147+ when :write
148+ args = [ nil , [ tcp ] , nil , WRITE_TIMEOUT ]
149+ else
150+ raise "Unknown select type #{ type } "
151+ end
152+ IO . select ( *args ) || raise ( "Socket timeout during #{ type } " )
153+ end
154+
70155 # Forward any methods directly to SSLSocket
71156 def method_missing ( method_sym , *arguments , &block )
72157 @socket . send ( method_sym , *arguments , &block )
0 commit comments