Skip to content

Commit ef6508f

Browse files
committed
ssl: support IO-like object as the underlying transport
OpenSSL::SSL::SSLSocket currently requires a real IO (socket) object because it passes the file descriptor to OpenSSL. OpenSSL internally uses an I/O abstraction layer called BIO to interact with the underlying socket. BIO is pluggable; the implementation can be provided by a user application. It's possible to implement our own BIO implementation ("BIO method") that wraps any Ruby IO-like object. Let's do it. This allows establishing a TLS connection on top of another TLS connection. For the performance reason, this patch continues to use the original socket BIO if the user passes a real IO object.
1 parent 2a83105 commit ef6508f

File tree

3 files changed

+205
-22
lines changed

3 files changed

+205
-22
lines changed

ext/openssl/ossl_ssl.c

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,13 +1607,28 @@ peeraddr_ip_str(VALUE self)
16071607
return rb_rescue2(peer_ip_address, self, fallback_peer_ip_address, (VALUE)0, rb_eSystemCallError, NULL);
16081608
}
16091609

1610+
static int
1611+
is_real_socket(VALUE io)
1612+
{
1613+
return false;
1614+
return RB_TYPE_P(io, T_FILE);// && false;
1615+
}
1616+
16101617
/*
16111618
* call-seq:
16121619
* SSLSocket.new(io) => aSSLSocket
16131620
* SSLSocket.new(io, ctx) => aSSLSocket
16141621
*
1615-
* Creates a new SSL socket from _io_ which must be a real IO object (not an
1616-
* IO-like object that responds to read/write).
1622+
* Creates a new SSL socket from _io_ which must be an IO object
1623+
* or an IO-like object that at least implements the following methods:
1624+
*
1625+
* - <tt>write_nonblock</tt> with <tt>exception: false</tt>
1626+
* - <tt>read_nonblock</tt> with <tt>exception: false</tt>
1627+
* - <tt>wait_readable</tt>
1628+
* - <tt>wait_writable</tt>
1629+
* - <tt>flush</tt>
1630+
* - <tt>close</tt>
1631+
* - <tt>closed?</tt>
16171632
*
16181633
* If _ctx_ is provided the SSL Sockets initial params will be taken from
16191634
* the context.
@@ -1641,9 +1656,20 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self)
16411656
rb_ivar_set(self, id_i_context, v_ctx);
16421657
ossl_sslctx_setup(v_ctx);
16431658

1644-
if (rb_respond_to(io, rb_intern("nonblock=")))
1645-
rb_funcall(io, rb_intern("nonblock="), 1, Qtrue);
1646-
Check_Type(io, T_FILE);
1659+
if (is_real_socket(io)) {
1660+
rb_io_t *fptr;
1661+
GetOpenFile(io, fptr);
1662+
rb_io_set_nonblock(fptr);
1663+
}
1664+
else {
1665+
// Not meant to be a comprehensive check
1666+
if (!rb_respond_to(io, rb_intern("read_nonblock")) ||
1667+
!rb_respond_to(io, rb_intern("write_nonblock")))
1668+
rb_raise(rb_eTypeError, "io must be a real IO object or an IO-like "
1669+
"object that responds to read_nonblock and write_nonblock");
1670+
if (rb_respond_to(io, rb_intern("nonblock=")))
1671+
rb_funcall(io, rb_intern("nonblock="), 1, Qtrue);
1672+
}
16471673
rb_ivar_set(self, id_i_io, io);
16481674

16491675
ssl = SSL_new(ctx);
@@ -1679,18 +1705,28 @@ ossl_ssl_setup(VALUE self)
16791705
{
16801706
VALUE io;
16811707
SSL *ssl;
1682-
rb_io_t *fptr;
16831708

16841709
GetSSL(self, ssl);
16851710
if (ssl_started(ssl))
16861711
return Qtrue;
16871712

16881713
io = rb_attr_get(self, id_i_io);
1689-
GetOpenFile(io, fptr);
1690-
rb_io_check_readable(fptr);
1691-
rb_io_check_writable(fptr);
1692-
if (!SSL_set_fd(ssl, TO_SOCKET(rb_io_descriptor(io))))
1693-
ossl_raise(eSSLError, "SSL_set_fd");
1714+
if (is_real_socket(io)) {
1715+
rb_io_t *fptr;
1716+
GetOpenFile(io, fptr);
1717+
rb_io_check_readable(fptr);
1718+
rb_io_check_writable(fptr);
1719+
if (!SSL_set_fd(ssl, TO_SOCKET(rb_io_descriptor(io))))
1720+
ossl_raise(eSSLError, "SSL_set_fd");
1721+
}
1722+
else {
1723+
BIO *bio = BIO_new(ossl_bio_meth);
1724+
if (!bio)
1725+
ossl_raise(eSSLError, "BIO_new(ossl_bio_meth)");
1726+
BIO_set_data(bio, (void *)io);
1727+
// Returns void currently (but wouldn't it be technically possible to fail?)
1728+
SSL_set_bio(ssl, bio, bio);
1729+
}
16941730

16951731
return Qtrue;
16961732
}
@@ -1701,6 +1737,32 @@ ossl_ssl_setup(VALUE self)
17011737
#define ssl_get_error(ssl, ret) SSL_get_error((ssl), (ret))
17021738
#endif
17031739

1740+
static void
1741+
handle_ossl_bio_error(SSL *ssl, BIO *bio, int ret)
1742+
{
1743+
int state = ossl_bio_restore_error(bio);
1744+
if (!state)
1745+
return;
1746+
1747+
/*
1748+
* Operation may succeed while the underlying socket reports
1749+
* an error in one corner case: TLS 1.3 server tries to send a
1750+
* NewSessionTicket on a closed socket (IOW, when the client
1751+
* disconnects right after finishing a handshake).
1752+
*
1753+
* According to ssl/statem/statem_srvr.c conn_is_closed(), EPIPE and
1754+
* ECONNRESET may be ignored.
1755+
*/
1756+
int error_code = ssl_get_error(ssl, ret);
1757+
if ((ret > 0 || error_code == SSL_ERROR_ZERO_RETURN || error_code == SSL_ERROR_SSL) &&
1758+
rb_obj_is_kind_of(rb_errinfo(), rb_eSystemCallError)) {
1759+
rb_set_errinfo(Qnil);
1760+
return;
1761+
}
1762+
ossl_clear_error();
1763+
rb_jump_tag(state);
1764+
}
1765+
17041766
static void
17051767
write_would_block(int nonblock)
17061768
{
@@ -1739,6 +1801,11 @@ no_exception_p(VALUE opts)
17391801
static void
17401802
io_wait_writable(VALUE io)
17411803
{
1804+
if (!is_real_socket(io)) {
1805+
if (!RTEST(rb_funcallv(io, rb_intern("wait_writable"), 0, NULL)))
1806+
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!");
1807+
return;
1808+
}
17421809
#ifdef HAVE_RB_IO_MAYBE_WAIT
17431810
if (!rb_io_maybe_wait_writable(errno, io, RUBY_IO_TIMEOUT_DEFAULT)) {
17441811
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!");
@@ -1753,6 +1820,11 @@ io_wait_writable(VALUE io)
17531820
static void
17541821
io_wait_readable(VALUE io)
17551822
{
1823+
if (!is_real_socket(io)) {
1824+
if (!RTEST(rb_funcallv(io, rb_intern("wait_readable"), 0, NULL)))
1825+
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!");
1826+
return;
1827+
}
17561828
#ifdef HAVE_RB_IO_MAYBE_WAIT
17571829
if (!rb_io_maybe_wait_readable(errno, io, RUBY_IO_TIMEOUT_DEFAULT)) {
17581830
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!");
@@ -1777,8 +1849,12 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts)
17771849
GetSSL(self, ssl);
17781850

17791851
VALUE io = rb_attr_get(self, id_i_io);
1852+
BIO *bio = SSL_get_rbio(ssl);
1853+
17801854
for (;;) {
17811855
ret = func(ssl);
1856+
if (!is_real_socket(io))
1857+
handle_ossl_bio_error(ssl, bio, ret);
17821858

17831859
cb_state = rb_attr_get(self, ID_callback_state);
17841860
if (!NIL_P(cb_state)) {
@@ -1967,10 +2043,14 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock)
19672043
return str;
19682044

19692045
VALUE io = rb_attr_get(self, id_i_io);
2046+
BIO *bio = SSL_get_rbio(ssl);
19702047

19712048
rb_str_locktmp(str);
19722049
for (;;) {
19732050
int nread = SSL_read(ssl, RSTRING_PTR(str), ilen);
2051+
if (!is_real_socket(io))
2052+
handle_ossl_bio_error(ssl, bio, nread);
2053+
19742054
switch (ssl_get_error(ssl, nread)) {
19752055
case SSL_ERROR_NONE:
19762056
rb_str_unlocktmp(str);
@@ -2067,6 +2147,7 @@ ossl_ssl_write_internal(VALUE self, VALUE str, VALUE opts)
20672147

20682148
tmp = rb_str_new_frozen(StringValue(str));
20692149
VALUE io = rb_attr_get(self, id_i_io);
2150+
BIO *bio = SSL_get_rbio(ssl);
20702151

20712152
/* SSL_write(3ssl) manpage states num == 0 is undefined */
20722153
num = RSTRING_LENINT(tmp);
@@ -2075,6 +2156,9 @@ ossl_ssl_write_internal(VALUE self, VALUE str, VALUE opts)
20752156

20762157
for (;;) {
20772158
int nwritten = SSL_write(ssl, RSTRING_PTR(tmp), num);
2159+
if (!is_real_socket(io))
2160+
handle_ossl_bio_error(ssl, bio, nwritten);
2161+
20782162
switch (ssl_get_error(ssl, nwritten)) {
20792163
case SSL_ERROR_NONE:
20802164
return INT2NUM(nwritten);
@@ -2152,7 +2236,15 @@ ossl_ssl_stop(VALUE self)
21522236
GetSSL(self, ssl);
21532237
if (!ssl_started(ssl))
21542238
return Qnil;
2239+
21552240
ret = SSL_shutdown(ssl);
2241+
2242+
/* XXX: Suppressing errors from the underlying socket */
2243+
VALUE io = rb_attr_get(self, id_i_io);
2244+
BIO *bio = SSL_get_rbio(ssl);
2245+
if (!is_real_socket(io) && ossl_bio_restore_error(bio))
2246+
rb_set_errinfo(Qnil);
2247+
21562248
if (ret == 1) /* Have already received close_notify */
21572249
return Qnil;
21582250
if (ret == 0) /* Sent close_notify, but we don't wait for reply */

test/openssl/test_pair.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,32 @@ def create_tcp_client(host, port)
6767
end
6868
end
6969

70+
module OpenSSL::SSLPairIOish
71+
include OpenSSL::SSLPairM
72+
73+
def create_tcp_server(host, port)
74+
Addrinfo.tcp(host, port).listen
75+
end
76+
77+
class TCPSocketWrapper
78+
def initialize(io) @io = io end
79+
def read_nonblock(*args, **kwargs) @io.read_nonblock(*args, **kwargs) end
80+
def write_nonblock(*args, **kwargs) @io.write_nonblock(*args, **kwargs) end
81+
def wait_readable() @io.wait_readable end
82+
def wait_writable() @io.wait_writable end
83+
def flush() @io.flush end
84+
def close() @io.close end
85+
def closed?() @io.closed? end
86+
87+
# Only used within test_pair.rb
88+
def write(*args) @io.write(*args) end
89+
end
90+
91+
def create_tcp_client(host, port)
92+
TCPSocketWrapper.new(Addrinfo.tcp(host, port).connect)
93+
end
94+
end
95+
7096
module OpenSSL::TestEOF1M
7197
def open_file(content)
7298
ssl_pair { |s1, s2|
@@ -492,6 +518,12 @@ class OpenSSL::TestEOF1LowlevelSocket < OpenSSL::TestCase
492518
include OpenSSL::TestEOF1M
493519
end
494520

521+
class OpenSSL::TestEOF1IOish < OpenSSL::TestCase
522+
include OpenSSL::TestEOF
523+
include OpenSSL::SSLPairIOish
524+
include OpenSSL::TestEOF1M
525+
end
526+
495527
class OpenSSL::TestEOF2 < OpenSSL::TestCase
496528
include OpenSSL::TestEOF
497529
include OpenSSL::SSLPair
@@ -504,6 +536,12 @@ class OpenSSL::TestEOF2LowlevelSocket < OpenSSL::TestCase
504536
include OpenSSL::TestEOF2M
505537
end
506538

539+
class OpenSSL::TestEOF2IOish < OpenSSL::TestCase
540+
include OpenSSL::TestEOF
541+
include OpenSSL::SSLPairIOish
542+
include OpenSSL::TestEOF2M
543+
end
544+
507545
class OpenSSL::TestPair < OpenSSL::TestCase
508546
include OpenSSL::SSLPair
509547
include OpenSSL::TestPairM
@@ -514,4 +552,9 @@ class OpenSSL::TestPairLowlevelSocket < OpenSSL::TestCase
514552
include OpenSSL::TestPairM
515553
end
516554

555+
class OpenSSL::TestPairIOish < OpenSSL::TestCase
556+
include OpenSSL::SSLPairIOish
557+
include OpenSSL::TestPairM
558+
end
559+
517560
end

test/openssl/test_ssl.rb

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,6 @@
44
if defined?(OpenSSL::SSL)
55

66
class OpenSSL::TestSSL < OpenSSL::SSLTestCase
7-
def test_bad_socket
8-
bad_socket = Struct.new(:sync).new
9-
assert_raise TypeError do
10-
socket = OpenSSL::SSL::SSLSocket.new bad_socket
11-
# if the socket is not a T_FILE, `connect` will segv because it tries
12-
# to get the underlying file descriptor but the API it calls assumes
13-
# the object type is T_FILE
14-
socket.connect
15-
end
16-
end
17-
187
def test_ctx_options
198
ctx = OpenSSL::SSL::SSLContext.new
209

@@ -117,6 +106,65 @@ def test_socket_open_with_local_address_port_context
117106
}
118107
end
119108

109+
def test_synthetic_io_sanity_check
110+
obj = Object.new
111+
assert_raise_with_message(TypeError, /read_nonblock/) { OpenSSL::SSL::SSLSocket.new(obj) }
112+
113+
obj = Object.new
114+
obj.define_singleton_method(:read_nonblock) { |*args, **kwargs| }
115+
obj.define_singleton_method(:write_nonblock) { |*args, **kwargs| }
116+
assert_nothing_raised { OpenSSL::SSL::SSLSocket.new(obj) }
117+
end
118+
119+
def test_synthetic_io
120+
start_server do |port|
121+
tcp = TCPSocket.new("127.0.0.1", port)
122+
obj = Object.new
123+
obj.define_singleton_method(:read_nonblock) { |maxlen, exception:|
124+
tcp.read_nonblock(maxlen, exception: exception) }
125+
obj.define_singleton_method(:write_nonblock) { |str, exception:|
126+
tcp.write_nonblock(str, exception: exception) }
127+
obj.define_singleton_method(:wait_readable) { tcp.wait_readable }
128+
obj.define_singleton_method(:wait_writable) { tcp.wait_writable }
129+
obj.define_singleton_method(:flush) { tcp.flush }
130+
obj.define_singleton_method(:closed?) { tcp.closed? }
131+
132+
ssl = OpenSSL::SSL::SSLSocket.new(obj)
133+
assert_same obj, ssl.to_io
134+
135+
ssl.connect
136+
ssl.puts "abc"; assert_equal "abc\n", ssl.gets
137+
ensure
138+
ssl&.close
139+
tcp&.close
140+
end
141+
end
142+
143+
def test_synthetic_io_write_nonblock_exception
144+
start_server(ignore_listener_error: true) do |port|
145+
tcp = TCPSocket.new("127.0.0.1", port)
146+
obj = Object.new
147+
[:read_nonblock, :wait_readable, :wait_writable, :flush, :closed?].each do |name|
148+
obj.define_singleton_method(name) { |*args, **kwargs|
149+
tcp.__send__(name, *args, **kwargs) }
150+
end
151+
152+
# SSLSocket#connect calls write_nonblock at least twice: ClientHello and Finished
153+
# Let's break the second call
154+
called = 0
155+
obj.define_singleton_method(:write_nonblock) { |*args, **kwargs|
156+
raise "foo" if (called += 1) == 2
157+
tcp.write_nonblock(*args, **kwargs)
158+
}
159+
160+
ssl = OpenSSL::SSL::SSLSocket.new(obj)
161+
assert_raise_with_message(RuntimeError, "foo") { ssl.connect }
162+
ensure
163+
ssl&.close
164+
tcp&.close
165+
end
166+
end
167+
120168
def test_add_certificate
121169
ctx_proc = -> ctx {
122170
# Unset values set by start_server

0 commit comments

Comments
 (0)