Skip to content

Commit 84ead32

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 supplied by a user application as long as it implements the necessary BIO functions. We can make our own BIO implementation ("BIO method") that wraps any Ruby IO-like object using normal Ruby method calls. Support for such an IO-like object is useful for establishing TLS connections on top of non-OS sockets, such as another TLS connection or an HTTP/2 tunnel. For performance reason, this patch continues to use the original socket BIO if the user passes a real IO object.
1 parent 0ff6d21 commit 84ead32

File tree

3 files changed

+205
-23
lines changed

3 files changed

+205
-23
lines changed

ext/openssl/ossl_ssl.c

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,7 +1551,11 @@ static void
15511551
ossl_ssl_mark(void *ptr)
15521552
{
15531553
SSL *ssl = ptr;
1554-
rb_gc_mark((VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx));
1554+
VALUE obj = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx);
1555+
1556+
// Ensure GC compaction won't move objects referenced by OpenSSL objects
1557+
rb_gc_mark(obj);
1558+
rb_gc_mark(rb_attr_get(obj, id_i_io));
15551559
}
15561560

15571561
static void
@@ -1601,13 +1605,29 @@ peeraddr_ip_str(VALUE self)
16011605
return rb_rescue2(peer_ip_address, self, fallback_peer_ip_address, (VALUE)0, rb_eSystemCallError, NULL);
16021606
}
16031607

1608+
static int
1609+
is_real_socket(VALUE io)
1610+
{
1611+
// FIXME: DO NOT MERGE
1612+
return 0;
1613+
return RB_TYPE_P(io, T_FILE);
1614+
}
1615+
16041616
/*
16051617
* call-seq:
16061618
* SSLSocket.new(io) => aSSLSocket
16071619
* SSLSocket.new(io, ctx) => aSSLSocket
16081620
*
1609-
* Creates a new SSL socket from _io_ which must be a real IO object (not an
1610-
* IO-like object that responds to read/write).
1621+
* Creates a new SSL socket from _io_ which must be an IO object
1622+
* or an IO-like object that at least implements the following methods:
1623+
*
1624+
* - <tt>write_nonblock</tt> with <tt>exception: false</tt>
1625+
* - <tt>read_nonblock</tt> with <tt>exception: false</tt>
1626+
* - <tt>wait_readable</tt>
1627+
* - <tt>wait_writable</tt>
1628+
* - <tt>flush</tt>
1629+
* - <tt>close</tt>
1630+
* - <tt>closed?</tt>
16111631
*
16121632
* If _ctx_ is provided the SSL Sockets initial params will be taken from
16131633
* the context.
@@ -1635,9 +1655,18 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self)
16351655
rb_ivar_set(self, id_i_context, v_ctx);
16361656
ossl_sslctx_setup(v_ctx);
16371657

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

16431672
ssl = SSL_new(ctx);
@@ -1669,18 +1698,24 @@ ossl_ssl_setup(VALUE self)
16691698
{
16701699
VALUE io;
16711700
SSL *ssl;
1672-
rb_io_t *fptr;
16731701

16741702
GetSSL(self, ssl);
16751703
if (ssl_started(ssl))
16761704
return Qtrue;
16771705

16781706
io = rb_attr_get(self, id_i_io);
1679-
GetOpenFile(io, fptr);
1680-
rb_io_check_readable(fptr);
1681-
rb_io_check_writable(fptr);
1682-
if (!SSL_set_fd(ssl, TO_SOCKET(rb_io_descriptor(io))))
1683-
ossl_raise(eSSLError, "SSL_set_fd");
1707+
if (is_real_socket(io)) {
1708+
rb_io_t *fptr;
1709+
GetOpenFile(io, fptr);
1710+
rb_io_check_readable(fptr);
1711+
rb_io_check_writable(fptr);
1712+
if (!SSL_set_fd(ssl, TO_SOCKET(rb_io_descriptor(io))))
1713+
ossl_raise(eSSLError, "SSL_set_fd");
1714+
}
1715+
else {
1716+
BIO *bio = ossl_bio_new(io);
1717+
SSL_set_bio(ssl, bio, bio);
1718+
}
16841719

16851720
return Qtrue;
16861721
}
@@ -1691,6 +1726,38 @@ ossl_ssl_setup(VALUE self)
16911726
#define ssl_get_error(ssl, ret) SSL_get_error((ssl), (ret))
16921727
#endif
16931728

1729+
static void
1730+
check_bio_error(SSL *ssl, VALUE io, int ret)
1731+
{
1732+
if (is_real_socket(io))
1733+
return;
1734+
1735+
BIO *bio = SSL_get_rbio(ssl);
1736+
int state = ossl_bio_state(bio);
1737+
if (!state)
1738+
return;
1739+
1740+
/*
1741+
* Operation may succeed while the underlying socket reports an error in
1742+
* some cases. For example, when TLS 1.3 server tries to send a
1743+
* NewSessionTicket on a closed socket (IOW, when the client disconnects
1744+
* right after finishing a handshake).
1745+
*
1746+
* According to ssl/statem/statem_srvr.c conn_is_closed(), EPIPE and
1747+
* ECONNRESET may be ignored.
1748+
*
1749+
* FIXME BEFORE MERGE: Currently ignoring all SystemCallError.
1750+
*/
1751+
int error_code = SSL_get_error(ssl, ret);
1752+
if ((ret > 0 || error_code == SSL_ERROR_ZERO_RETURN || error_code == SSL_ERROR_SSL) &&
1753+
rb_obj_is_kind_of(rb_errinfo(), rb_eSystemCallError)) {
1754+
rb_set_errinfo(Qnil);
1755+
return;
1756+
}
1757+
ossl_clear_error();
1758+
rb_jump_tag(state);
1759+
}
1760+
16941761
static void
16951762
write_would_block(int nonblock)
16961763
{
@@ -1729,6 +1796,11 @@ no_exception_p(VALUE opts)
17291796
static void
17301797
io_wait_writable(VALUE io)
17311798
{
1799+
if (!is_real_socket(io)) {
1800+
if (!RTEST(rb_funcallv(io, rb_intern("wait_writable"), 0, NULL)))
1801+
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!");
1802+
return;
1803+
}
17321804
#ifdef HAVE_RB_IO_MAYBE_WAIT
17331805
if (!rb_io_maybe_wait_writable(errno, io, RUBY_IO_TIMEOUT_DEFAULT)) {
17341806
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!");
@@ -1743,6 +1815,11 @@ io_wait_writable(VALUE io)
17431815
static void
17441816
io_wait_readable(VALUE io)
17451817
{
1818+
if (!is_real_socket(io)) {
1819+
if (!RTEST(rb_funcallv(io, rb_intern("wait_readable"), 0, NULL)))
1820+
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!");
1821+
return;
1822+
}
17461823
#ifdef HAVE_RB_IO_MAYBE_WAIT
17471824
if (!rb_io_maybe_wait_readable(errno, io, RUBY_IO_TIMEOUT_DEFAULT)) {
17481825
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!");
@@ -1767,8 +1844,10 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts)
17671844
GetSSL(self, ssl);
17681845

17691846
VALUE io = rb_attr_get(self, id_i_io);
1847+
17701848
for (;;) {
17711849
ret = func(ssl);
1850+
check_bio_error(ssl, io, ret);
17721851

17731852
cb_state = rb_attr_get(self, ID_callback_state);
17741853
if (!NIL_P(cb_state)) {
@@ -1963,6 +2042,8 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock)
19632042
rb_str_locktmp(str);
19642043
for (;;) {
19652044
int nread = SSL_read(ssl, RSTRING_PTR(str), ilen);
2045+
check_bio_error(ssl, io, nread);
2046+
19662047
switch (ssl_get_error(ssl, nread)) {
19672048
case SSL_ERROR_NONE:
19682049
rb_str_unlocktmp(str);
@@ -2067,6 +2148,8 @@ ossl_ssl_write_internal(VALUE self, VALUE str, VALUE opts)
20672148

20682149
for (;;) {
20692150
int nwritten = SSL_write(ssl, RSTRING_PTR(tmp), num);
2151+
check_bio_error(ssl, io, nwritten);
2152+
20702153
switch (ssl_get_error(ssl, nwritten)) {
20712154
case SSL_ERROR_NONE:
20722155
return INT2NUM(nwritten);
@@ -2144,7 +2227,15 @@ ossl_ssl_stop(VALUE self)
21442227
GetSSL(self, ssl);
21452228
if (!ssl_started(ssl))
21462229
return Qnil;
2230+
21472231
ret = SSL_shutdown(ssl);
2232+
2233+
/* XXX: Suppressing errors from the underlying socket */
2234+
VALUE io = rb_attr_get(self, id_i_io);
2235+
BIO *bio = SSL_get_rbio(ssl);
2236+
if (!is_real_socket(io) && ossl_bio_state(bio))
2237+
rb_set_errinfo(Qnil);
2238+
21482239
if (ret == 1) /* Have already received close_notify */
21492240
return Qnil;
21502241
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|
@@ -518,6 +544,12 @@ class OpenSSL::TestEOF1LowlevelSocket < OpenSSL::TestCase
518544
include OpenSSL::TestEOF1M
519545
end
520546

547+
class OpenSSL::TestEOF1IOish < OpenSSL::TestCase
548+
include OpenSSL::TestEOF
549+
include OpenSSL::SSLPairIOish
550+
include OpenSSL::TestEOF1M
551+
end
552+
521553
class OpenSSL::TestEOF2 < OpenSSL::TestCase
522554
include OpenSSL::TestEOF
523555
include OpenSSL::SSLPair
@@ -530,6 +562,12 @@ class OpenSSL::TestEOF2LowlevelSocket < OpenSSL::TestCase
530562
include OpenSSL::TestEOF2M
531563
end
532564

565+
class OpenSSL::TestEOF2IOish < OpenSSL::TestCase
566+
include OpenSSL::TestEOF
567+
include OpenSSL::SSLPairIOish
568+
include OpenSSL::TestEOF2M
569+
end
570+
533571
class OpenSSL::TestPair < OpenSSL::TestCase
534572
include OpenSSL::SSLPair
535573
include OpenSSL::TestPairM
@@ -540,4 +578,9 @@ class OpenSSL::TestPairLowlevelSocket < OpenSSL::TestCase
540578
include OpenSSL::TestPairM
541579
end
542580

581+
class OpenSSL::TestPairIOish < OpenSSL::TestCase
582+
include OpenSSL::SSLPairIOish
583+
include OpenSSL::TestPairM
584+
end
585+
543586
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

@@ -141,6 +130,65 @@ def test_socket_close_write
141130
end
142131
end
143132

133+
def test_synthetic_io_sanity_check
134+
obj = Object.new
135+
assert_raise_with_message(TypeError, /read_nonblock/) { OpenSSL::SSL::SSLSocket.new(obj) }
136+
137+
obj = Object.new
138+
obj.define_singleton_method(:read_nonblock) { |*args, **kwargs| }
139+
obj.define_singleton_method(:write_nonblock) { |*args, **kwargs| }
140+
assert_nothing_raised { OpenSSL::SSL::SSLSocket.new(obj) }
141+
end
142+
143+
def test_synthetic_io
144+
start_server do |port|
145+
tcp = TCPSocket.new("127.0.0.1", port)
146+
obj = Object.new
147+
obj.define_singleton_method(:read_nonblock) { |maxlen, exception:|
148+
tcp.read_nonblock(maxlen, exception: exception) }
149+
obj.define_singleton_method(:write_nonblock) { |str, exception:|
150+
tcp.write_nonblock(str, exception: exception) }
151+
obj.define_singleton_method(:wait_readable) { tcp.wait_readable }
152+
obj.define_singleton_method(:wait_writable) { tcp.wait_writable }
153+
obj.define_singleton_method(:flush) { tcp.flush }
154+
obj.define_singleton_method(:closed?) { tcp.closed? }
155+
156+
ssl = OpenSSL::SSL::SSLSocket.new(obj)
157+
assert_same obj, ssl.to_io
158+
159+
ssl.connect
160+
ssl.puts "abc"; assert_equal "abc\n", ssl.gets
161+
ensure
162+
ssl&.close
163+
tcp&.close
164+
end
165+
end
166+
167+
def test_synthetic_io_write_nonblock_exception
168+
start_server(ignore_listener_error: true) do |port|
169+
tcp = TCPSocket.new("127.0.0.1", port)
170+
obj = Object.new
171+
[:read_nonblock, :wait_readable, :wait_writable, :flush, :closed?].each do |name|
172+
obj.define_singleton_method(name) { |*args, **kwargs|
173+
tcp.__send__(name, *args, **kwargs) }
174+
end
175+
176+
# SSLSocket#connect calls write_nonblock at least twice: ClientHello and Finished
177+
# Let's break the second call
178+
called = 0
179+
obj.define_singleton_method(:write_nonblock) { |*args, **kwargs|
180+
raise "foo" if (called += 1) == 2
181+
tcp.write_nonblock(*args, **kwargs)
182+
}
183+
184+
ssl = OpenSSL::SSL::SSLSocket.new(obj)
185+
assert_raise_with_message(RuntimeError, "foo") { ssl.connect }
186+
ensure
187+
ssl&.close
188+
tcp&.close
189+
end
190+
end
191+
144192
def test_add_certificate
145193
ctx_proc = -> ctx {
146194
# Unset values set by start_server

0 commit comments

Comments
 (0)