Class: Socketry::SSL::Socket

Inherits:
TCP::Socket show all
Defined in:
lib/socketry/ssl/socket.rb

Overview

SSL Sockets

Constant Summary

Constants included from Timeout

Timeout::DEFAULT_TIMEOUTS, Timeout::DEFAULT_TIMER

Instance Attribute Summary

Attributes inherited from TCP::Socket

#addr_fmaily, #local_addr, #local_port, #read_timeout, #remote_addr, #remote_port, #resolver, #socket_class, #write_timeout

Instance Method Summary collapse

Methods inherited from TCP::Socket

#closed?, connect, #nodelay, #nodelay=, #read, #readpartial, #reconnect, #to_io, #write, #writepartial

Methods included from Timeout

#clear_timeout, #lifetime, #set_timeout, #start_timer, #time_remaining

Constructor Details

#initialize(ssl_socket_class: OpenSSL::SSL::SSLSocket, ssl_context: OpenSSL::SSL::SSLContext.new, ssl_params: nil, **args) ⇒ Socketry::SSL::Socket

Create an unconnected Socketry::SSL::Socket

Parameters:

  • read_timeout (Numeric)

    Seconds to wait before an uncompleted read errors

  • write_timeout (Numeric)

    Seconds to wait before an uncompleted write errors

  • timer (Object)

    A timekeeping object to use for measuring timeouts

  • resolver (Object)

    A resolver object to use for resolving DNS names

  • socket_class (Object)

    Underlying socket class which implements I/O ops

  • ssl_socket_class (Object) (defaults to: OpenSSL::SSL::SSLSocket)

    Class which provides the underlying SSL implementation

  • ssl_context (OpenSSL::SSL::SSLContext) (defaults to: OpenSSL::SSL::SSLContext.new)

    SSL configuration object

  • ssL_params (Hash)

    Parameter hash to set on the given SSL context

Raises:

  • (TypeError)


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/socketry/ssl/socket.rb', line 20

def initialize(
  ssl_socket_class: OpenSSL::SSL::SSLSocket,
  ssl_context: OpenSSL::SSL::SSLContext.new,
  ssl_params: nil,
  **args
)
  raise TypeError, "invalid SSL context (#{ssl_context.class})" unless ssl_context.is_a?(OpenSSL::SSL::SSLContext)
  raise TypeError, "expected Hash, got #{ssl_params.class}" if ssl_params && !ssl_params.is_a?(Hash)

  @ssl_socket_class = ssl_socket_class

  @ssl_context = ssl_context
  @ssl_context.set_params(ssl_params) if ssl_params && !ssl_params.empty?

  @ssl_socket = nil

  super(**args)
end

Instance Method Details

#accept(tcp_socket, timeout: nil) ⇒ self

Accept an SSL connection from a Socketry or Ruby socket

Parameters:

  • tcp_socket (TCPSocket, Socketry::TCP::Socket)

    raw TCP socket to begin SSL handshake with

  • timeout (Numeric, NilClass) (defaults to: nil)

    (default nil, unlimited) seconds to wait before aborting the accept

Returns:

  • (self)


102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/socketry/ssl/socket.rb', line 102

def accept(tcp_socket, timeout: nil)
  tcp_socket = IO.try_convert(tcp_socket) || raise(TypeError, "couldn't convert #{tcp_socket.class} to IO")
  ssl_socket = @ssl_socket_class.new(tcp_socket, @ssl_context)

  begin
    ssl_socket.accept_nonblock
  rescue IO::WaitReadable
    retry if IO.select([tcp_socket], nil, nil, timeout)
    raise Socketry::TimeoutError, "failed to complete handshake after #{timeout} seconds"
  rescue IO::WaitWritable
    retry if IO.select(nil, [tcp_socket], nil, timeout)
    raise Socketry::TimeoutError, "failed to complete handshake after #{timeout} seconds"
  end

  from_socket(ssl_socket)
end

#closetrue, false

Close the socket

Returns:

  • (true, false)

    true if the socket was open, false if closed



165
166
167
168
169
170
# File 'lib/socketry/ssl/socket.rb', line 165

def close
  return false unless super
  @ssl_socket.close
  @ssl_socket = nil
  true
end

#connect(remote_addr, remote_port, local_addr: nil, local_port: nil, timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect], enable_sni: true, verify_hostname: true) ⇒ self

Make an SSL connection to a remote host

Parameters:

  • remote_addr (String)

    DNS name or IP address of the host to connect to

  • remote_port (Fixnum)

    TCP port to connect to

  • local_addr (String) (defaults to: nil)

    DNS name or IP address to bind to locally

  • local_port (Fixnum) (defaults to: nil)

    Local TCP port to bind to

  • timeout (Numeric) (defaults to: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect])

    Number of seconds to wait before aborting connect

  • enable_sni (true, false) (defaults to: true)

    (default: true) Enables Server Name Indication (SNI)

  • verify_hostname (true, false) (defaults to: true)

    (default: true) Ensure server’s hostname matches cert

Returns:

  • (self)

Raises:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/socketry/ssl/socket.rb', line 53

def connect(
  remote_addr,
  remote_port,
  local_addr: nil,
  local_port: nil,
  timeout: Socketry::Timeout::DEFAULT_TIMEOUTS[:connect],
  enable_sni: true,
  verify_hostname: true
)
  super(remote_addr, remote_port, local_addr: local_addr, local_port: local_port, timeout: timeout)

  @ssl_socket = @ssl_socket_class.new(@socket, @ssl_context)
  @ssl_socket.hostname = remote_addr if enable_sni
  @ssl_socket.sync_close = true

  begin
    @ssl_socket.connect_nonblock
  rescue IO::WaitReadable
    retry if @socket.wait_readable(timeout)
    raise Socketry::TimeoutError, "connection to #{remote_addr}:#{remote_port} timed out"
  rescue IO::WaitWritable
    retry if @socket.wait_writable(timeout)
    raise Socketry::TimeoutError, "connection to #{remote_addr}:#{remote_port} timed out"
  rescue OpenSSL::SSL::SSLError => ex
    raise Socketry::SSL::CertificateVerifyError, ex.message if ex.message.include?("certificate verify failed")
    raise Socketry::SSL::Error, ex.message, ex.backtrace
  end

  begin
    @ssl_socket.post_connection_check(remote_addr) if verify_hostname
  rescue OpenSSL::SSL::SSLError => ex
    raise Socketry::SSL::HostnameError, ex.message, ex.backtrace
  end

  self
rescue => ex
  @socket.close rescue nil
  @socket = nil
  @ssl_socket.close rescue nil
  @ssl_socket = nil
  raise ex
end

#from_socket(ssl_socket) ⇒ self

Wrap a Ruby OpenSSL::SSL::SSLSocket (or other low-level SSL socket)

Parameters:

  • ssl_socket (OpenSSL::SSL::SSLSocket)

    SSL socket class associated with this socket

Returns:

  • (self)

Raises:

  • (TypeError)


124
125
126
127
128
129
130
131
132
133
# File 'lib/socketry/ssl/socket.rb', line 124

def from_socket(ssl_socket)
  raise TypeError, "expected #{@ssl_socket_class}, got #{ssl_socket.class}" unless ssl_socket.is_a?(@ssl_socket_class)
  raise StateError, "already connected" if @socket

  @socket = ssl_socket.to_io
  @ssl_socket = ssl_socket
  @ssl_socket.sync_close = true

  self
end

#read_nonblock(size, outbuf: nil) ⇒ String, :wait_readable

Perform a non-blocking read operation

Parameters:

  • size (Fixnum)

    number of bytes to attempt to read

  • outbuf (String, NilClass) (defaults to: nil)

    an optional buffer into which data should be read

Returns:

  • (String, :wait_readable)

    data read, or :wait_readable if operation would block

Raises:



142
143
144
145
146
147
148
149
150
# File 'lib/socketry/ssl/socket.rb', line 142

def read_nonblock(size, outbuf: nil)
  case outbuf
  when String
    perform { @ssl_socket.read_nonblock(size, outbuf, exception: false) }
  when NilClass
    perform { @ssl_socket.read_nonblock(size, exception: false) }
  else raise TypeError, "unexpected outbuf class: #{outbuf.class}"
  end
end

#write_nonblock(data) ⇒ Fixnum, :wait_writable

Perform a non-blocking write operation

Parameters:

  • data (String)

    number of bytes to attempt to read

Returns:

  • (Fixnum, :wait_writable)

    number of bytes written, or :wait_writable if op would block

Raises:



158
159
160
# File 'lib/socketry/ssl/socket.rb', line 158

def write_nonblock(data)
  perform { @ssl_socket.write_nonblock(data, exception: false) }
end