Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 52 additions & 19 deletions ext/openssl/ossl_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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");
}
}

/*
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 15 additions & 3 deletions lib/openssl/ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions test/openssl/test_ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading