diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 91aadf0b6..d1047a4c1 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -37,7 +37,7 @@ static VALUE eSSLErrorWaitReadable; static VALUE eSSLErrorWaitWritable; static ID id_call, ID_callback_state, id_npn_protocols_encoded, id_each; -static VALUE sym_exception, sym_wait_readable, sym_wait_writable; +static VALUE sym_wait_readable, sym_wait_writable; static ID id_i_cert_store, id_i_ca_file, id_i_ca_path, id_i_verify_mode, id_i_verify_depth, id_i_verify_callback, id_i_client_ca, @@ -1772,255 +1772,99 @@ errno_mapped(void) #endif } -static void -write_would_block(int nonblock) -{ - if (nonblock) - ossl_raise(eSSLErrorWaitWritable, "write would block"); -} - -static void -read_would_block(int nonblock) -{ - if (nonblock) - ossl_raise(eSSLErrorWaitReadable, "read would block"); -} - -static int -no_exception_p(VALUE opts) -{ - if (RB_TYPE_P(opts, T_HASH) && - rb_hash_lookup2(opts, sym_exception, Qundef) == Qfalse) - return 1; - return 0; -} - -// Provided by Ruby 3.2.0 and later in order to support the default IO#timeout. -#ifndef RUBY_IO_TIMEOUT_DEFAULT -#define RUBY_IO_TIMEOUT_DEFAULT Qnil -#endif - -#ifdef HAVE_RB_IO_TIMEOUT -#define IO_TIMEOUT_ERROR rb_eIOTimeoutError -#else -#define IO_TIMEOUT_ERROR rb_eIOError -#endif - - -static void -io_wait_writable(VALUE io) -{ -#ifdef HAVE_RB_IO_MAYBE_WAIT - if (!rb_io_wait(io, INT2NUM(RUBY_IO_WRITABLE), RUBY_IO_TIMEOUT_DEFAULT)) { - rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!"); - } -#else - rb_io_t *fptr; - GetOpenFile(io, fptr); - rb_thread_fd_writable(fptr->fd); -#endif -} - -static void -io_wait_readable(VALUE io) -{ -#ifdef HAVE_RB_IO_MAYBE_WAIT - if (!rb_io_wait(io, INT2NUM(RUBY_IO_READABLE), RUBY_IO_TIMEOUT_DEFAULT)) { - rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!"); - } -#else - rb_io_t *fptr; - GetOpenFile(io, fptr); - rb_thread_wait_fd(fptr->fd); -#endif -} - static VALUE -ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) +ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname) { SSL *ssl; VALUE cb_state; - int nonblock = opts != Qfalse; rb_ivar_set(self, ID_callback_state, Qnil); GetSSL(self, ssl); - VALUE io = rb_attr_get(self, id_i_io); - for (;;) { - int ret = func(ssl); - int saved_errno = errno_mapped(); +#ifdef __APPLE__ + retry:; +#endif + int ret = func(ssl); + int saved_errno = errno_mapped(); - cb_state = rb_attr_get(self, ID_callback_state); - if (!NIL_P(cb_state)) { - /* must cleanup OpenSSL error stack before re-raising */ - ossl_clear_error(); - rb_jump_tag(NUM2INT(cb_state)); - } + cb_state = rb_attr_get(self, ID_callback_state); + if (!NIL_P(cb_state)) { + /* must cleanup OpenSSL error stack before re-raising */ + ossl_clear_error(); + rb_jump_tag(NUM2INT(cb_state)); + } + + if (ret > 0) + return self; - if (ret > 0) - break; - - int code = SSL_get_error(ssl, ret); - switch (code) { - case SSL_ERROR_WANT_WRITE: - if (no_exception_p(opts)) { return sym_wait_writable; } - write_would_block(nonblock); - io_wait_writable(io); - continue; - case SSL_ERROR_WANT_READ: - if (no_exception_p(opts)) { return sym_wait_readable; } - read_would_block(nonblock); - io_wait_readable(io); - continue; - case SSL_ERROR_SYSCALL: + int code = SSL_get_error(ssl, ret); + switch (code) { + case SSL_ERROR_WANT_WRITE: + return sym_wait_writable; + case SSL_ERROR_WANT_READ: + return sym_wait_readable; + case SSL_ERROR_SYSCALL: #ifdef __APPLE__ - /* See ossl_ssl_write_internal() */ - if (saved_errno == EPROTOTYPE) - continue; + /* See ossl_ssl_write_internal() */ + if (saved_errno == EPROTOTYPE) + goto retry; #endif - if (saved_errno) - rb_exc_raise(rb_syserr_new(saved_errno, funcname)); - /* fallthrough */ - default: { - VALUE error_append = Qnil; + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, funcname)); + /* fallthrough */ + default: { + VALUE error_append = Qnil; #if defined(SSL_R_CERTIFICATE_VERIFY_FAILED) - unsigned long err = ERR_peek_last_error(); - if (ERR_GET_LIB(err) == ERR_LIB_SSL && - ERR_GET_REASON(err) == SSL_R_CERTIFICATE_VERIFY_FAILED) { - const char *err_msg = ERR_reason_error_string(err), - *verify_msg = X509_verify_cert_error_string(SSL_get_verify_result(ssl)); - if (!err_msg) - err_msg = "(null)"; - if (!verify_msg) - verify_msg = "(null)"; - ossl_clear_error(); /* let ossl_raise() not append message */ - error_append = rb_sprintf(": %s (%s)", err_msg, verify_msg); - } -#endif - ossl_raise(eSSLError, - "%s%s returned=%d errno=%d peeraddr=%"PRIsVALUE" state=%s%"PRIsVALUE, - funcname, - code == SSL_ERROR_SYSCALL ? " SYSCALL" : "", - code, - saved_errno, - peeraddr_ip_str(io), - SSL_state_string_long(ssl), - error_append); + unsigned long err = ERR_peek_last_error(); + if (ERR_GET_LIB(err) == ERR_LIB_SSL && + ERR_GET_REASON(err) == SSL_R_CERTIFICATE_VERIFY_FAILED) { + long verify_result = SSL_get_verify_result(ssl); + const char *err_msg = ERR_reason_error_string(err), + *verify_msg = X509_verify_cert_error_string(verify_result); + if (!err_msg) + err_msg = "(null)"; + if (!verify_msg) + verify_msg = "(null)"; + ossl_clear_error(); /* let ossl_raise() not append message */ + error_append = rb_sprintf(": %s (%s)", err_msg, verify_msg); } - } +#endif + VALUE io = rb_attr_get(self, id_i_io); + ossl_raise(eSSLError, + "%s%s returned=%d errno=%d peeraddr=%"PRIsVALUE" state=%s%"PRIsVALUE, + funcname, + code == SSL_ERROR_SYSCALL ? " SYSCALL" : "", + code, + saved_errno, + peeraddr_ip_str(io), + SSL_state_string_long(ssl), + error_append); + } } - - return self; } -/* - * call-seq: - * ssl.connect => self - * - * Initiates an SSL/TLS handshake with a server. - */ static VALUE ossl_ssl_connect(VALUE self) { ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_connect, "SSL_connect", Qfalse); + return ossl_start_ssl(self, SSL_connect, "SSL_connect"); } -/* - * call-seq: - * ssl.connect_nonblock([options]) => self - * - * Initiates the SSL/TLS handshake as a client in non-blocking manner. - * - * # emulates blocking connect - * begin - * ssl.connect_nonblock - * rescue IO::WaitReadable - * IO.select([s2]) - * retry - * rescue IO::WaitWritable - * IO.select(nil, [s2]) - * retry - * end - * - * By specifying a keyword argument _exception_ to +false+, you can indicate - * that connect_nonblock should not raise an IO::WaitReadable or - * IO::WaitWritable exception, but return the symbol +:wait_readable+ or - * +:wait_writable+ instead. - */ -static VALUE -ossl_ssl_connect_nonblock(int argc, VALUE *argv, VALUE self) -{ - VALUE opts; - rb_scan_args(argc, argv, "0:", &opts); - - ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_connect, "SSL_connect", opts); -} - -/* - * call-seq: - * ssl.accept => self - * - * Waits for a SSL/TLS client to initiate a handshake. - */ static VALUE ossl_ssl_accept(VALUE self) { ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_accept, "SSL_accept", Qfalse); + return ossl_start_ssl(self, SSL_accept, "SSL_accept"); } -/* - * call-seq: - * ssl.accept_nonblock([options]) => self - * - * Initiates the SSL/TLS handshake as a server in non-blocking manner. - * - * # emulates blocking accept - * begin - * ssl.accept_nonblock - * rescue IO::WaitReadable - * IO.select([s2]) - * retry - * rescue IO::WaitWritable - * IO.select(nil, [s2]) - * retry - * end - * - * By specifying a keyword argument _exception_ to +false+, you can indicate - * that accept_nonblock should not raise an IO::WaitReadable or - * IO::WaitWritable exception, but return the symbol +:wait_readable+ or - * +:wait_writable+ instead. - */ static VALUE -ossl_ssl_accept_nonblock(int argc, VALUE *argv, VALUE self) -{ - VALUE opts; - - rb_scan_args(argc, argv, "0:", &opts); - ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_accept, "SSL_accept", opts); -} - -static VALUE -ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) +ossl_ssl_read(VALUE self, VALUE len, VALUE str) { SSL *ssl; int ilen; - VALUE len, str, cb_state; - VALUE opts = Qnil; + VALUE cb_state; - if (nonblock) { - rb_scan_args(argc, argv, "11:", &len, &str, &opts); - } else { - rb_scan_args(argc, argv, "11", &len, &str); - } GetSSL(self, ssl); if (!ssl_started(ssl)) rb_raise(eSSLError, "SSL session is not started yet"); @@ -2041,224 +1885,109 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) return str; } - VALUE io = rb_attr_get(self, id_i_io); + rb_str_locktmp(str); + int nread = SSL_read(ssl, RSTRING_PTR(str), ilen); + rb_str_unlocktmp(str); + int saved_errno = errno_mapped(); - for (;;) { - rb_str_locktmp(str); - int nread = SSL_read(ssl, RSTRING_PTR(str), ilen); - int saved_errno = errno_mapped(); - rb_str_unlocktmp(str); - - cb_state = rb_attr_get(self, ID_callback_state); - if (!NIL_P(cb_state)) { - rb_ivar_set(self, ID_callback_state, Qnil); - ossl_clear_error(); - rb_jump_tag(NUM2INT(cb_state)); - } + cb_state = rb_attr_get(self, ID_callback_state); + if (!NIL_P(cb_state)) { + rb_ivar_set(self, ID_callback_state, Qnil); + ossl_clear_error(); + rb_jump_tag(NUM2INT(cb_state)); + } - switch (SSL_get_error(ssl, nread)) { - case SSL_ERROR_NONE: - rb_str_set_len(str, nread); - return str; - case SSL_ERROR_ZERO_RETURN: - if (no_exception_p(opts)) { return Qnil; } - rb_eof_error(); - case SSL_ERROR_WANT_WRITE: - if (nonblock) { - if (no_exception_p(opts)) { return sym_wait_writable; } - write_would_block(nonblock); - } - io_wait_writable(io); - break; - case SSL_ERROR_WANT_READ: - if (nonblock) { - if (no_exception_p(opts)) { return sym_wait_readable; } - read_would_block(nonblock); - } - io_wait_readable(io); - break; - case SSL_ERROR_SYSCALL: - if (!ERR_peek_error()) { - if (saved_errno) - rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); - else { - /* - * The underlying BIO returned 0. This is actually a - * protocol error. But unfortunately, not all - * implementations cleanly shutdown the TLS connection - * but just shutdown/close the TCP connection. So report - * EOF for now... - */ - if (no_exception_p(opts)) { return Qnil; } - rb_eof_error(); - } + switch (SSL_get_error(ssl, nread)) { + case SSL_ERROR_NONE: + rb_str_set_len(str, nread); + return str; + case SSL_ERROR_ZERO_RETURN: + return Qnil; + case SSL_ERROR_WANT_WRITE: + return sym_wait_writable; + case SSL_ERROR_WANT_READ: + return sym_wait_readable; + case SSL_ERROR_SYSCALL: + if (!ERR_peek_error()) { + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); + else { + /* + * The underlying BIO returned 0. This is actually a + * protocol error. But unfortunately, not all + * implementations cleanly shutdown the TLS connection + * but just shutdown/close the TCP connection. So report + * EOF for now... + */ + return Qnil; } - /* fall through */ - default: - ossl_raise(eSSLError, "SSL_read"); } - - // Ensure the buffer is not modified during io_wait_*able() - rb_str_modify(str); - if (rb_str_capacity(str) < (size_t)ilen) - rb_raise(eSSLError, "read buffer was modified"); + /* fall through */ + default: + ossl_raise(eSSLError, "SSL_read"); } } -/* - * call-seq: - * ssl.sysread(length) => string - * ssl.sysread(length, buffer) => buffer - * - * Reads _length_ bytes from the SSL connection. If a pre-allocated _buffer_ - * is provided the data will be written into it. - */ static VALUE -ossl_ssl_read(int argc, VALUE *argv, VALUE self) -{ - return ossl_ssl_read_internal(argc, argv, self, 0); -} - -/* - * call-seq: - * ssl.sysread_nonblock(length) => string - * ssl.sysread_nonblock(length, buffer) => buffer - * ssl.sysread_nonblock(length[, buffer [, opts]) => buffer - * - * A non-blocking version of #sysread. Raises an SSLError if reading would - * block. If "exception: false" is passed, this method returns a symbol of - * :wait_readable, :wait_writable, or nil, rather than raising an exception. - * - * Reads _length_ bytes from the SSL connection. If a pre-allocated _buffer_ - * is provided the data will be written into it. - */ -static VALUE -ossl_ssl_read_nonblock(int argc, VALUE *argv, VALUE self) -{ - return ossl_ssl_read_internal(argc, argv, self, 1); -} - -static VALUE -ossl_ssl_write_internal_safe(VALUE _args) +ossl_ssl_write(VALUE self, VALUE str) { - VALUE *args = (VALUE*)_args; - VALUE self = args[0]; - VALUE str = args[1]; - VALUE opts = args[2]; - SSL *ssl; - rb_io_t *fptr; - int num, nonblock = opts != Qfalse; VALUE cb_state; GetSSL(self, ssl); if (!ssl_started(ssl)) rb_raise(eSSLError, "SSL session is not started yet"); - VALUE io = rb_attr_get(self, id_i_io); - GetOpenFile(io, fptr); - + StringValue(str); /* SSL_write(3ssl) manpage states num == 0 is undefined */ - num = RSTRING_LENINT(str); + int num = RSTRING_LENINT(str); if (num == 0) return INT2FIX(0); - for (;;) { - int nwritten = SSL_write(ssl, RSTRING_PTR(str), num); - int saved_errno = errno_mapped(); - - cb_state = rb_attr_get(self, ID_callback_state); - if (!NIL_P(cb_state)) { - rb_ivar_set(self, ID_callback_state, Qnil); - ossl_clear_error(); - rb_jump_tag(NUM2INT(cb_state)); - } - - switch (SSL_get_error(ssl, nwritten)) { - case SSL_ERROR_NONE: - return INT2NUM(nwritten); - case SSL_ERROR_WANT_WRITE: - if (no_exception_p(opts)) { return sym_wait_writable; } - write_would_block(nonblock); - io_wait_writable(io); - continue; - case SSL_ERROR_WANT_READ: - if (no_exception_p(opts)) { return sym_wait_readable; } - read_would_block(nonblock); - io_wait_readable(io); - continue; - case SSL_ERROR_SYSCALL: #ifdef __APPLE__ - /* - * It appears that send syscall can return EPROTOTYPE if the - * socket is being torn down. Retry to get a proper errno to - * make the error handling in line with the socket library. - * [Bug #14713] https://bugs.ruby-lang.org/issues/14713 - */ - if (saved_errno == EPROTOTYPE) - continue; + retry:; #endif - if (saved_errno) - rb_exc_raise(rb_syserr_new(saved_errno, "SSL_write")); - /* fallthrough */ - default: - ossl_raise(eSSLError, "SSL_write"); - } - } -} - - -static VALUE -ossl_ssl_write_internal(VALUE self, VALUE str, VALUE opts) -{ - StringValue(str); int frozen = RB_OBJ_FROZEN(str); if (!frozen) { rb_str_locktmp(str); } - int state; - VALUE args[3] = {self, str, opts}; - VALUE result = rb_protect(ossl_ssl_write_internal_safe, (VALUE)args, &state); + int nwritten = SSL_write(ssl, RSTRING_PTR(str), num); if (!frozen) { rb_str_unlocktmp(str); } + int saved_errno = errno_mapped(); - if (state) { - rb_jump_tag(state); + cb_state = rb_attr_get(self, ID_callback_state); + if (!NIL_P(cb_state)) { + rb_ivar_set(self, ID_callback_state, Qnil); + ossl_clear_error(); + rb_jump_tag(NUM2INT(cb_state)); } - return result; -} -/* - * call-seq: - * ssl.syswrite(string) => Integer - * - * Writes _string_ to the SSL connection. - */ -static VALUE -ossl_ssl_write(VALUE self, VALUE str) -{ - return ossl_ssl_write_internal(self, str, Qfalse); -} - -/* - * call-seq: - * ssl.syswrite_nonblock(string) => Integer - * ssl.syswrite_nonblock(string, opts) => Integer - * - * Writes _string_ to the SSL connection in a non-blocking manner. Raises an - * SSLError if writing would block. If "exception: false" is passed, this - * method returns a symbol of :wait_readable or :wait_writable, rather than - * raising an exception. - */ -static VALUE -ossl_ssl_write_nonblock(int argc, VALUE *argv, VALUE self) -{ - VALUE str, opts; - - rb_scan_args(argc, argv, "1:", &str, &opts); - - return ossl_ssl_write_internal(self, str, opts); + switch (SSL_get_error(ssl, nwritten)) { + case SSL_ERROR_NONE: + return INT2NUM(nwritten); + case SSL_ERROR_WANT_WRITE: + return sym_wait_writable; + case SSL_ERROR_WANT_READ: + return sym_wait_readable; + case SSL_ERROR_SYSCALL: +#ifdef __APPLE__ + /* + * It appears that send syscall can return EPROTOTYPE if the + * socket is being torn down. Retry to get a proper errno to + * make the error handling in line with the socket library. + * [Bug #14713] https://bugs.ruby-lang.org/issues/14713 + */ + if (saved_errno == EPROTOTYPE) + goto retry; +#endif + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_write")); + /* fallthrough */ + default: + ossl_raise(eSSLError, "SSL_write"); + } } /* @@ -3182,14 +2911,10 @@ Init_ossl_ssl(void) rb_define_alloc_func(cSSLSocket, ossl_ssl_s_alloc); rb_define_method(cSSLSocket, "initialize", ossl_ssl_initialize, -1); rb_undef_method(cSSLSocket, "initialize_copy"); - rb_define_method(cSSLSocket, "connect", ossl_ssl_connect, 0); - rb_define_method(cSSLSocket, "connect_nonblock", ossl_ssl_connect_nonblock, -1); - rb_define_method(cSSLSocket, "accept", ossl_ssl_accept, 0); - rb_define_method(cSSLSocket, "accept_nonblock", ossl_ssl_accept_nonblock, -1); - rb_define_method(cSSLSocket, "sysread", ossl_ssl_read, -1); - rb_define_private_method(cSSLSocket, "sysread_nonblock", ossl_ssl_read_nonblock, -1); - rb_define_method(cSSLSocket, "syswrite", ossl_ssl_write, 1); - rb_define_private_method(cSSLSocket, "syswrite_nonblock", ossl_ssl_write_nonblock, -1); + rb_define_private_method(cSSLSocket, "ssl_connect", ossl_ssl_connect, 0); + rb_define_private_method(cSSLSocket, "ssl_accept", ossl_ssl_accept, 0); + rb_define_private_method(cSSLSocket, "ssl_read", ossl_ssl_read, 2); + rb_define_private_method(cSSLSocket, "ssl_write", ossl_ssl_write, 1); rb_define_private_method(cSSLSocket, "stop", ossl_ssl_stop, 0); rb_define_method(cSSLSocket, "cert", ossl_ssl_get_cert, 0); rb_define_method(cSSLSocket, "peer_cert", ossl_ssl_get_peer_cert, 0); @@ -3367,8 +3092,6 @@ Init_ossl_ssl(void) /* TLS 1.3 */ rb_define_const(mSSL, "TLS1_3_VERSION", INT2NUM(TLS1_3_VERSION)); - - sym_exception = ID2SYM(rb_intern_const("exception")); sym_wait_readable = ID2SYM(rb_intern_const("wait_readable")); sym_wait_writable = ID2SYM(rb_intern_const("wait_writable")); diff --git a/lib/openssl/ssl.rb b/lib/openssl/ssl.rb index 486bdf858..84e81dad4 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -207,10 +207,6 @@ def fcntl(*args) to_io.fcntl(*args) end - def closed? - to_io.closed? - end - def do_not_reverse_lookup=(flag) to_io.do_not_reverse_lookup = flag end @@ -347,6 +343,13 @@ class SSLSocket # connection is shut down. This defaults to +false+. attr_accessor :sync_close + # :stopdoc: + def initialize(*args) + super + @closed_read = @closed_write = false + end if RUBY_VERSION < "3.0" + # :startdoc: + # call-seq: # ssl.sysclose => nil # @@ -356,10 +359,212 @@ class SSLSocket # If sync_close is set to +true+, the underlying IO is also closed. def sysclose return if closed? + @closed_read = @closed_write = true stop io.close if sync_close end + # Ruby 3.2 + IO_TimeoutError = defined?(IO::TimeoutError) ? IO::TimeoutError : IOError + private_constant :IO_TimeoutError + + private def check_nonblock(ret) + case ret + when :wait_readable + raise SSLErrorWaitReadable, "read would block" + when :wait_writable + raise SSLErrorWaitWritable, "write would block" + when nil + raise EOFError, "end of file reached" + else + ret + end + end + + private def assert_readable + if @closed_read + raise IOError, "closed stream" if @closed_write + raise IOError, "not opened for reading" + end + end + + private def assert_writable + if @closed_write + raise IOError, "closed stream" if @closed_read + raise IOError, "not opened for writing" + end + end + + # :call-seq: + # ssl.connect -> self + # + # Initiates an SSL/TLS handshake with a server. + def connect + while true + case ret = ssl_connect + when :wait_readable + wait_readable or + raise IO_TimeoutError, "Timed out while waiting to become readable!" + when :wait_writable + wait_writable or + raise IO_TimeoutError, "Timed out while waiting to become writable!" + else + return ret + end + end + end + + # :call-seq: + # ssl.connect_nonblock -> self + # ssl.connect_nonblock(exception: false) -> self | :wait_readable | :wait_writable + # + # Initiates the SSL/TLS handshake as a client in non-blocking manner. + # + # # emulates blocking connect + # begin + # ssl.connect_nonblock + # rescue IO::WaitReadable + # IO.select([s2]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [s2]) + # retry + # end + # + # By specifying a keyword argument _exception_ to +false+, you can + # indicate that connect_nonblock should not raise an IO::WaitReadable or + # IO::WaitWritable exception, but return the symbol +:wait_readable+ or + # +:wait_writable+ instead. + def connect_nonblock(exception: true) + ret = ssl_connect + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.accept -> self + # + # Waits for a SSL/TLS client to initiate a handshake. + def accept + while true + case ret = ssl_accept + when :wait_readable + wait_readable or + raise IO_TimeoutError, "Timed out while waiting to become readable!" + when :wait_writable + wait_writable or + raise IO_TimeoutError, "Timed out while waiting to become writable!" + else + return ret + end + end + end + + # :call-seq: + # ssl.accept_nonblock -> self + # ssl.accept_nonblock(exception: false) -> self | :wait_readable | :wait_writable + # + # Initiates the SSL/TLS handshake as a server in non-blocking manner. + # + # # emulates blocking accept + # begin + # ssl.accept_nonblock + # rescue IO::WaitReadable + # IO.select([s2]) + # retry + # rescue IO::WaitWritable + # IO.select(nil, [s2]) + # retry + # end + # + # By specifying a keyword argument _exception_ to +false+, you can + # indicate that accept_nonblock should not raise an IO::WaitReadable or + # IO::WaitWritable exception, but return the symbol +:wait_readable+ or + # +:wait_writable+ instead. + def accept_nonblock(exception: true) + ret = ssl_accept + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.sysread(length) -> string + # ssl.sysread(length, buffer) -> buffer + # + # Reads _length_ bytes from the SSL connection. If a pre-allocated + # _buffer_ is provided the data will be written into it. + def sysread(length, buffer = nil) + assert_readable + while true + case ret = ssl_read(length, buffer) + when :wait_readable + wait_readable or + raise IO_TimeoutError, "Timed out while waiting to become readable!" + when :wait_writable + wait_writable or + raise IO_TimeoutError, "Timed out while waiting to become writable!" + when nil + raise EOFError, "end of file reached" + else + return ret + end + end + end + + # :call-seq: + # ssl.sysread_nonblock(length) -> string + # ssl.sysread_nonblock(length, buffer) -> buffer + # ssl.sysread_nonblock(length, buffer, exception: false) -> buffer | :wait_readable | :wait_writable | nil + # + # A non-blocking version of #sysread. Raises an SSLError if reading + # would block. If "exception: false" is passed, this method returns a + # symbol of :wait_readable, :wait_writable, or nil, rather than raising + # an exception. + # + # Reads _length_ bytes from the SSL connection. If a pre-allocated + # _buffer_ is provided the data will be written into it. + private def sysread_nonblock(length, buffer = nil, exception: true) + assert_readable + ret = ssl_read(length, buffer) + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.syswrite(string) -> Integer + # + # Writes _string_ to the SSL connection. + def syswrite(string) + assert_writable + while true + case ret = ssl_write(string) + when :wait_readable + wait_readable or + raise IO_TimeoutError, "Timed out while waiting to become readable!" + when :wait_writable + wait_writable or + raise IO_TimeoutError, "Timed out while waiting to become writable!" + else + return ret + end + end + end + + # :call-seq: + # ssl.syswrite_nonblock(string) -> Integer + # ssl.syswrite_nonblock(string, exception: false) -> Integer | :wait_readable | :wait_writable + # + # Writes _string_ to the SSL connection in a non-blocking manner. Raises + # an SSLError if writing would block. If "exception: false" is passed, + # this method returns a symbol of :wait_readable or :wait_writable, + # rather than raising an exception. + private def syswrite_nonblock(string, exception: true) + assert_writable + ret = ssl_write(string) + check_nonblock(ret) if exception + ret + end + # call-seq: # ssl.post_connection_check(hostname) -> true # @@ -394,16 +599,28 @@ def session nil end - # Close the stream for reading. - # This method is ignored by OpenSSL as there is no reasonable way to - # implement it, but exists for compatibility with IO. + # Returns +true+ if the SSL/TLS connection has been closed, +false+ + # otherwise. + # + # Before version 4.1, this method returned whether the underlying socket + # has been closed. + def closed? + !!(@closed_read && @closed_write) + end + + # Closes the stream for reading. Any further attempts to read from this + # SSLSocket will raise IOError. def close_read - # Unsupported and ignored. - # Just don't read any more. + return sysclose if @closed_write + @closed_read = true + nil end - # Closes the stream for writing. The behavior of this method depends on - # the version of OpenSSL and the TLS protocol in use. + # Closes the stream for writing. Any further attempts to write to this + # SSLSocket will raise IOError. + # + # The behavior of this method depends on the version of OpenSSL and the + # TLS protocol in use. # # - Sends a 'close_notify' alert to the peer. # - Does not wait for the peer's 'close_notify' alert in response. @@ -417,6 +634,8 @@ def close_read # completely shut down. On TLS 1.3, the connection will remain open for # reading only. def close_write + return sysclose if @closed_read + @closed_write = true stop end diff --git a/test/openssl/test_pair.rb b/test/openssl/test_pair.rb index 8cb1060e4..7e0974c51 100644 --- a/test/openssl/test_pair.rb +++ b/test/openssl/test_pair.rb @@ -386,10 +386,27 @@ def test_close_write message = "abc"*1024 s1.write(message) s1.close_write + assert_raise_with_message(IOError, /writing/) { s1.write(message) } assert_equal(message, s2.read) s2.write(message) s2.close_write - assert_equal(message, s1.read) + assert_equal(message, s1.read(message.bytesize)) + s1.close_read + assert_raise_with_message(IOError, /closed stream/) { s1.read(1) } + } + end + + def test_close_read + ssl_pair { |s1, s2| + message = "abc"*1024 + s1.close_read + s1.write(message) + assert_equal(message, s2.read(message.bytesize)) + s2.write(message) + s2.close_write + assert_raise_with_message(IOError, /reading/) { s1.read(1) } + s1.close_write + assert_raise_with_message(IOError, /closed stream/) { s1.write(message) } } end end diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index ae3c4502e..53a6bfead 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -388,6 +388,7 @@ def test_sync_close ssl.connect ssl.puts "abc"; assert_equal "abc\n", ssl.gets ssl.close + assert_predicate ssl, :closed? assert_not_predicate sock, :closed? ensure sock&.close @@ -400,6 +401,7 @@ def test_sync_close ssl.connect ssl.puts "abc"; assert_equal "abc\n", ssl.gets ssl.close + assert_predicate ssl, :closed? assert_predicate sock, :closed? ensure sock&.close