diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 825c2a17e..a5d601cf1 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1797,6 +1797,7 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname) cb_state = rb_attr_get(self, ID_callback_state); if (!NIL_P(cb_state)) { + rb_ivar_set(self, ID_callback_state, Qnil); /* must cleanup OpenSSL error stack before re-raising */ ossl_clear_error(); rb_jump_tag(NUM2INT(cb_state)); @@ -2025,34 +2026,66 @@ ossl_ssl_write(VALUE self, VALUE str) /* * call-seq: - * ssl.stop => nil + * ssl.ssl_shutdown -> 0, 1, :wait_readable, or :wait_writable + * + * A low-level method to shut down the connection. Read the man page + * SSL_shutdown(3) carefully since the semantics are complicated. + * + * The following description applies to TLS 1.3 and may not apply to other + * protocols. + * + * This method sends a "close notify" alert to the peer if not sent yet, and + * otherwise checks whether the peer has sent one in turn. + * + * Returns 0 if the alert has successfully been sent, 1 if the alert has been + * both sent and received. * - * Sends "close notify" to the peer and tries to shut down the SSL connection - * gracefully. + * This method is non-blocking. Returns +:wait_readable+ or +:wait_writable+ + * if the underlying socket is not ready for reading or writing. Raises + * SSLError or SystemCallError on non-recoverable errors. + * + * It is recommended to call this method only to send the alert, and not to + * wait for the peer's alert. SSLSocket's read methods report the peer's alert + * as EOF. */ static VALUE -ossl_ssl_stop(VALUE self) +ossl_ssl_shutdown(VALUE self) { SSL *ssl; - int ret; + int ret, saved_errno; + VALUE cb_state; GetSSL(self, ssl); if (!ssl_started(ssl)) - return Qnil; + rb_raise(eSSLError, "SSL session is not started yet"); + ret = SSL_shutdown(ssl); - if (ret == 1) /* Have already received close_notify */ - return Qnil; - if (ret == 0) /* Sent close_notify, but we don't wait for reply */ - return Qnil; + saved_errno = errno_mapped(); - /* - * XXX: Something happened. Possibly it failed because the underlying socket - * is not writable/readable, since it is in non-blocking mode. We should do - * some proper error handling using SSL_get_error() and maybe retry, but we - * can't block here. Give up for now. - */ - ossl_clear_error(); - return Qnil; + 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)); + } + + if (ret >= 0) + return INT2FIX(ret); + + switch (SSL_get_error(ssl, ret)) { + case SSL_ERROR_WANT_WRITE: + return sym_wait_writable; + case SSL_ERROR_WANT_READ: + return sym_wait_readable; + case SSL_ERROR_SYSCALL: + if (saved_errno) { + ossl_clear_error(); + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_shutdown")); + } + /* fallthrough */ + default: + ossl_raise(eSSLError, "SSL_shutdown"); + } } /* @@ -2956,7 +2989,7 @@ Init_ossl_ssl(void) rb_define_private_method(cSSLSocket, "string_new_capacity", ossl_ssl_string_new_capacity, 1); 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, "ssl_shutdown", ossl_ssl_shutdown, 0); rb_define_method(cSSLSocket, "cert", ossl_ssl_get_cert, 0); rb_define_method(cSSLSocket, "peer_cert", ossl_ssl_get_peer_cert, 0); rb_define_method(cSSLSocket, "peer_cert_chain", ossl_ssl_get_peer_cert_chain, 0); diff --git a/lib/openssl/ssl.rb b/lib/openssl/ssl.rb index 85a91dee2..c262363b6 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -369,13 +369,19 @@ class SSLSocket # call-seq: # ssl.sysclose => nil # - # Sends "close notify" to the peer and tries to shut down the SSL + # Tries to send "close notify" to the peer and shut down the SSL/TLS # connection gracefully. # + # If the underlying socket is not writable, the alert may not be sent, + # because #sysclose cannot block. See #ssl_shutdown if you need strict + # guarantees. + # # If sync_close is set to +true+, the underlying IO is also closed. def sysclose return if closed? - stop + ssl_shutdown + rescue SystemCallError, OpenSSL::SSL::SSLError + ensure io.close if sync_close end @@ -598,8 +604,14 @@ def close_read # Therefore, on TLS 1.2, this method will cause the connection to be # completely shut down. On TLS 1.3, the connection will remain open for # reading only. + # + # If the underlying socket is not writable, the alert may not be sent, + # because #close_write cannot block. See #ssl_shutdown if you need strict + # guarantees. def close_write - stop + ssl_shutdown + nil + rescue SystemCallError, OpenSSL::SSL::SSLError end private diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index fa220650b..2bad57445 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -1828,6 +1828,29 @@ def test_close_after_socket_close } end + def test_ssl_shutdown + server_can_close = Thread::Queue.new + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, make_server_context).accept + str = ssl.gets + ssl.puts(str) + assert_predicate(ssl, :eof?) + server_can_close.pop + assert_equal(1, ssl.ssl_shutdown) + end + start_server_proc(server_proc) do |port| + server_connect(port) do |ssl| + ssl.puts("abc") + assert_equal(0, ssl.ssl_shutdown) + assert_equal("abc\n", ssl.gets) + assert_equal(:wait_readable, ssl.ssl_shutdown) + server_can_close.close + ssl.wait_readable + assert_equal(1, ssl.ssl_shutdown) + end + end + end + def test_sync_close_without_connect Socket.open(:INET, :STREAM) {|s| ssl = OpenSSL::SSL::SSLSocket.new(s)