Skip to content

Commit 2acbebf

Browse files
committed
ssl: support IO-like object as an underlying transport
There are use cases to establish a TLS connection on top of a non-OS stream, such as another TLS connection or an HTTP/2 tunnel. To achieve this today, a workaround using dummy socket pairs is necessary. Currently, OpenSSL::SSL::SSLSocket.new requires an IO (socket) object backed by a file descriptor. This is because we pass the file descriptor to OpenSSL. This patch changes it to allow any Ruby object that responds to necessary non-blocking IO methods, such as read_nonblock. OpenSSL's TLS implementation uses an IO abstraction layer called BIO to interact with the underlying socket. By passing the file descriptor to SSL_set_fd(), a BIO with the BIO_s_socket() BIO_METHOD is implicitly created. We can set up our own BIO and let OpenSSL use it instead. The previous patch added such a BIO_METHOD implementation. For performance reason, this patch continues to use the socket BIO if the user passes a real IO object, so this should not change the behavior of existing programs in any way.
1 parent 845e65a commit 2acbebf

File tree

3 files changed

+218
-59
lines changed

3 files changed

+218
-59
lines changed

ext/openssl/ossl_ssl.c

Lines changed: 116 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ static VALUE eSSLErrorWaitReadable;
3737
static VALUE eSSLErrorWaitWritable;
3838

3939
static ID id_call, ID_callback_state, id_tmp_dh_callback,
40-
id_npn_protocols_encoded, id_each;
40+
id_npn_protocols_encoded, id_each, id_bio;
4141
static VALUE sym_exception, sym_wait_readable, sym_wait_writable;
4242

4343
static ID id_i_cert_store, id_i_ca_file, id_i_ca_path, id_i_verify_mode,
@@ -1557,13 +1557,21 @@ peeraddr_ip_str(VALUE self)
15571557
rb_eSystemCallError, (VALUE)0);
15581558
}
15591559

1560+
static int
1561+
is_real_socket(VALUE io)
1562+
{
1563+
return RB_TYPE_P(io, T_FILE);
1564+
}
1565+
15601566
/*
15611567
* call-seq:
15621568
* SSLSocket.new(io) => aSSLSocket
15631569
* SSLSocket.new(io, ctx) => aSSLSocket
15641570
*
1565-
* Creates a new SSL socket from _io_ which must be a real IO object (not an
1566-
* IO-like object that responds to read/write).
1571+
* Creates a new SSL socket from the underlying socket _io_ and _ctx_.
1572+
*
1573+
* _io_ must be an IO object, typically a TCPSocket or Socket from the socket
1574+
* library, or an IO-like object that supports the typical IO methods.
15671575
*
15681576
* If _ctx_ is provided the SSL Sockets initial params will be taken from
15691577
* the context.
@@ -1572,6 +1580,22 @@ peeraddr_ip_str(VALUE self)
15721580
*
15731581
* This method will freeze the SSLContext if one is provided;
15741582
* however, session management is still allowed in the frozen SSLContext.
1583+
*
1584+
* == Support for IO-like objects
1585+
*
1586+
* Support for IO-like objects was added in version 3.3 and is experimental.
1587+
*
1588+
* As of version 3.3, SSLSocket uses the following methods:
1589+
*
1590+
* - <tt>write_nonblock</tt> with the <tt>exception: false</tt> option
1591+
* - <tt>read_nonblock</tt> with the <tt>exception: false</tt> option
1592+
* - <tt>wait_readable</tt>
1593+
* - <tt>wait_writable</tt>
1594+
* - <tt>flush</tt>
1595+
* - <tt>close</tt>
1596+
* - <tt>closed?</tt>
1597+
*
1598+
* Note that future versions may require additional methods to be implemented.
15751599
*/
15761600
static VALUE
15771601
ossl_ssl_initialize(int argc, VALUE *argv, VALUE self)
@@ -1591,9 +1615,18 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self)
15911615
rb_ivar_set(self, id_i_context, v_ctx);
15921616
ossl_sslctx_setup(v_ctx);
15931617

1594-
if (rb_respond_to(io, rb_intern("nonblock=")))
1595-
rb_funcall(io, rb_intern("nonblock="), 1, Qtrue);
1596-
Check_Type(io, T_FILE);
1618+
if (is_real_socket(io)) {
1619+
rb_io_t *fptr;
1620+
GetOpenFile(io, fptr);
1621+
rb_io_set_nonblock(fptr);
1622+
}
1623+
else {
1624+
// Not meant to be a comprehensive check
1625+
if (!rb_respond_to(io, rb_intern("read_nonblock")) ||
1626+
!rb_respond_to(io, rb_intern("write_nonblock")))
1627+
rb_raise(rb_eTypeError, "io must be a real IO object or an IO-like "
1628+
"object that responds to read_nonblock and write_nonblock");
1629+
}
15971630
rb_ivar_set(self, id_i_io, io);
15981631

15991632
ssl = SSL_new(ctx);
@@ -1625,27 +1658,59 @@ ossl_ssl_setup(VALUE self)
16251658
{
16261659
VALUE io;
16271660
SSL *ssl;
1628-
rb_io_t *fptr;
16291661

16301662
GetSSL(self, ssl);
16311663
if (ssl_started(ssl))
16321664
return Qtrue;
16331665

16341666
io = rb_attr_get(self, id_i_io);
1635-
GetOpenFile(io, fptr);
1636-
rb_io_check_readable(fptr);
1637-
rb_io_check_writable(fptr);
1638-
if (!SSL_set_fd(ssl, TO_SOCKET(rb_io_descriptor(io))))
1639-
ossl_raise(eSSLError, "SSL_set_fd");
1667+
if (is_real_socket(io)) {
1668+
rb_io_t *fptr;
1669+
GetOpenFile(io, fptr);
1670+
rb_io_check_readable(fptr);
1671+
rb_io_check_writable(fptr);
1672+
if (!SSL_set_fd(ssl, TO_SOCKET(rb_io_descriptor(io))))
1673+
ossl_raise(eSSLError, "SSL_set_fd");
1674+
}
1675+
else {
1676+
VALUE bobj = ossl_bio_new(io);
1677+
rb_ivar_set(self, id_bio, bobj);
1678+
1679+
BIO *bio = ossl_bio_get(bobj);
1680+
if (!BIO_up_ref(bio))
1681+
ossl_raise(eSSLError, "BIO_up_ref");
1682+
SSL_set_bio(ssl, bio, bio);
1683+
}
16401684

16411685
return Qtrue;
16421686
}
16431687

1688+
static void
1689+
check_bio_error(VALUE self, SSL *ssl, VALUE bobj, int ret)
1690+
{
1691+
VALUE cb_state = rb_attr_get(self, ID_callback_state);
1692+
if (!NIL_P(cb_state)) {
1693+
/* must cleanup OpenSSL error stack before re-raising */
1694+
ossl_clear_error();
1695+
rb_jump_tag(NUM2INT(cb_state));
1696+
}
1697+
1698+
// Socket BIO -> nothing to do
1699+
if (NIL_P(bobj)) {
16441700
#ifdef _WIN32
1645-
#define ssl_get_error(ssl, ret) (errno = rb_w32_map_errno(WSAGetLastError()), SSL_get_error((ssl), (ret)))
1646-
#else
1647-
#define ssl_get_error(ssl, ret) SSL_get_error((ssl), (ret))
1701+
errno = rb_w32_map_errno(WSAGetLastError());
16481702
#endif
1703+
return;
1704+
}
1705+
1706+
int state = ossl_bio_state(bobj);
1707+
if (!state) {
1708+
errno = 0;
1709+
return;
1710+
}
1711+
ossl_clear_error();
1712+
rb_jump_tag(state);
1713+
}
16491714

16501715
static void
16511716
write_would_block(int nonblock)
@@ -1685,6 +1750,11 @@ no_exception_p(VALUE opts)
16851750
static void
16861751
io_wait_writable(VALUE io)
16871752
{
1753+
if (!is_real_socket(io)) {
1754+
if (!RTEST(rb_funcallv(io, rb_intern("wait_writable"), 0, NULL)))
1755+
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!");
1756+
return;
1757+
}
16881758
#ifdef HAVE_RB_IO_MAYBE_WAIT
16891759
if (!rb_io_maybe_wait_writable(errno, io, RUBY_IO_TIMEOUT_DEFAULT)) {
16901760
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!");
@@ -1699,6 +1769,11 @@ io_wait_writable(VALUE io)
16991769
static void
17001770
io_wait_readable(VALUE io)
17011771
{
1772+
if (!is_real_socket(io)) {
1773+
if (!RTEST(rb_funcallv(io, rb_intern("wait_readable"), 0, NULL)))
1774+
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!");
1775+
return;
1776+
}
17021777
#ifdef HAVE_RB_IO_MAYBE_WAIT
17031778
if (!rb_io_maybe_wait_readable(errno, io, RUBY_IO_TIMEOUT_DEFAULT)) {
17041779
rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!");
@@ -1715,28 +1790,22 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts)
17151790
{
17161791
SSL *ssl;
17171792
int ret, ret2;
1718-
VALUE cb_state;
17191793
int nonblock = opts != Qfalse;
17201794

1721-
rb_ivar_set(self, ID_callback_state, Qnil);
1722-
17231795
GetSSL(self, ssl);
17241796

1725-
VALUE io = rb_attr_get(self, id_i_io);
1797+
VALUE io = rb_attr_get(self, id_i_io),
1798+
bobj = rb_attr_get(self, id_bio);
1799+
1800+
rb_ivar_set(self, ID_callback_state, Qnil);
17261801
for (;;) {
17271802
ret = func(ssl);
1728-
1729-
cb_state = rb_attr_get(self, ID_callback_state);
1730-
if (!NIL_P(cb_state)) {
1731-
/* must cleanup OpenSSL error stack before re-raising */
1732-
ossl_clear_error();
1733-
rb_jump_tag(NUM2INT(cb_state));
1734-
}
1803+
check_bio_error(self, ssl, bobj, ret);
17351804

17361805
if (ret > 0)
17371806
break;
17381807

1739-
switch ((ret2 = ssl_get_error(ssl, ret))) {
1808+
switch ((ret2 = SSL_get_error(ssl, ret))) {
17401809
case SSL_ERROR_WANT_WRITE:
17411810
if (no_exception_p(opts)) { return sym_wait_writable; }
17421811
write_would_block(nonblock);
@@ -1886,7 +1955,7 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock)
18861955
{
18871956
SSL *ssl;
18881957
int ilen;
1889-
VALUE len, str, cb_state;
1958+
VALUE len, str;
18901959
VALUE opts = Qnil;
18911960

18921961
if (nonblock) {
@@ -1914,21 +1983,17 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock)
19141983
return str;
19151984
}
19161985

1917-
VALUE io = rb_attr_get(self, id_i_io);
1986+
VALUE io = rb_attr_get(self, id_i_io),
1987+
bobj = rb_attr_get(self, id_bio);
19181988

1989+
rb_ivar_set(self, ID_callback_state, Qnil);
19191990
for (;;) {
19201991
rb_str_locktmp(str);
19211992
int nread = SSL_read(ssl, RSTRING_PTR(str), ilen);
19221993
rb_str_unlocktmp(str);
1994+
check_bio_error(self, ssl, bobj, nread);
19231995

1924-
cb_state = rb_attr_get(self, ID_callback_state);
1925-
if (!NIL_P(cb_state)) {
1926-
rb_ivar_set(self, ID_callback_state, Qnil);
1927-
ossl_clear_error();
1928-
rb_jump_tag(NUM2INT(cb_state));
1929-
}
1930-
1931-
switch (ssl_get_error(ssl, nread)) {
1996+
switch (SSL_get_error(ssl, nread)) {
19321997
case SSL_ERROR_NONE:
19331998
rb_str_set_len(str, nread);
19341999
return str;
@@ -2020,30 +2085,25 @@ ossl_ssl_write_internal_safe(VALUE _args)
20202085

20212086
SSL *ssl;
20222087
int num, nonblock = opts != Qfalse;
2023-
VALUE cb_state;
20242088

20252089
GetSSL(self, ssl);
20262090
if (!ssl_started(ssl))
20272091
rb_raise(eSSLError, "SSL session is not started yet");
20282092

2029-
VALUE io = rb_attr_get(self, id_i_io);
2030-
20312093
/* SSL_write(3ssl) manpage states num == 0 is undefined */
20322094
num = RSTRING_LENINT(str);
20332095
if (num == 0)
20342096
return INT2FIX(0);
20352097

2098+
VALUE io = rb_attr_get(self, id_i_io),
2099+
bobj = rb_attr_get(self, id_bio);
2100+
2101+
rb_ivar_set(self, ID_callback_state, Qnil);
20362102
for (;;) {
20372103
int nwritten = SSL_write(ssl, RSTRING_PTR(str), num);
2104+
check_bio_error(self, ssl, bobj, nwritten);
20382105

2039-
cb_state = rb_attr_get(self, ID_callback_state);
2040-
if (!NIL_P(cb_state)) {
2041-
rb_ivar_set(self, ID_callback_state, Qnil);
2042-
ossl_clear_error();
2043-
rb_jump_tag(NUM2INT(cb_state));
2044-
}
2045-
2046-
switch (ssl_get_error(ssl, nwritten)) {
2106+
switch (SSL_get_error(ssl, nwritten)) {
20472107
case SSL_ERROR_NONE:
20482108
return INT2NUM(nwritten);
20492109
case SSL_ERROR_WANT_WRITE:
@@ -2142,7 +2202,14 @@ ossl_ssl_stop(VALUE self)
21422202
GetSSL(self, ssl);
21432203
if (!ssl_started(ssl))
21442204
return Qnil;
2205+
21452206
ret = SSL_shutdown(ssl);
2207+
2208+
/* XXX: Suppressing errors from the underlying socket */
2209+
VALUE bobj = rb_attr_get(self, id_bio);
2210+
if (!NIL_P(bobj) && ossl_bio_state(bobj))
2211+
rb_set_errinfo(Qnil);
2212+
21462213
if (ret == 1) /* Have already received close_notify */
21472214
return Qnil;
21482215
if (ret == 0) /* Sent close_notify, but we don't wait for reply */
@@ -3120,6 +3187,7 @@ Init_ossl_ssl(void)
31203187
id_tmp_dh_callback = rb_intern_const("tmp_dh_callback");
31213188
id_npn_protocols_encoded = rb_intern_const("npn_protocols_encoded");
31223189
id_each = rb_intern_const("each");
3190+
id_bio = rb_intern_const("bio");
31233191

31243192
#define DefIVarID(name) do \
31253193
id_i_##name = rb_intern_const("@"#name); while (0)

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

0 commit comments

Comments
 (0)