diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index e58764a26..e7d2d18ec 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)); @@ -2027,34 +2028,70 @@ ossl_ssl_write(VALUE self, VALUE str) /* * call-seq: - * ssl.stop => nil + * ssl.ssl_shutdown -> 0, 1, :wait_readable, or :wait_writable + * + * Calls SSL_shutdown() once. This is a low-level method called by #close + * and #close_write. + * + * Returns 0 or 1 as SSL_shutdown() does. This method never blocks. It returns + * +:wait_readable+ or +:wait_writable+ if the underlying socket is not ready, + * and raises SSLError or SystemCallError on other, unrecoverable errors. + * + * The following description applies to TLS 1.3 and may not apply to other + * protocols. + * + * If a "close notify" alert has not been sent to the peer yet, this method + * sends one. On success, it returns 0 if the peer's alert has not been + * received yet, or 1 if it has. * - * Sends "close notify" to the peer and tries to shut down the SSL connection - * gracefully. + * If a "close notify" alert has already been sent, this method checks whether + * the peer has sent one in turn. If the next record received from the peer is + * application data, this method fails with an unrecoverable error and the data + * is discarded. + * + * It is recommended to call this method only to send the alert. To receive + * the peer's alert, use SSLSocket's read methods until they reach EOF. + * + * See also the man page SSL_shutdown(3). */ 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"); + } } /* @@ -2958,7 +2995,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..886f81823 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -369,13 +369,20 @@ 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 + nil + rescue SystemCallError, OpenSSL::SSL::SSLError + ensure io.close if sync_close end @@ -598,8 +605,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..b4cc9ab70 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -1810,7 +1810,7 @@ def test_npn_selected_protocol_too_long end def test_close_after_socket_close - # The client closes the TCP socket without SSLSocket#stop + # The client closes the TCP socket without calling SSLSocket#close sctx = make_server_context if defined?(OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF) sctx.options |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF @@ -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)