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
75 changes: 56 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 @@ -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");
}
}

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

Expand Down Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion test/openssl/test_ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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