diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index fcbbec0b3..d0cea374c 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, @@ -1757,255 +1757,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(); - - 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)); - } + int ret; +#ifdef __APPLE__ + retry: +#endif + 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)); + } + + 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) { + 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 + 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); -} - -/* - * 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_connect_nonblock(VALUE self) { ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_accept, "SSL_accept", Qfalse); + return ossl_start_ssl(self, SSL_connect, "SSL_connect"); } -/* - * 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) +ossl_ssl_accept_nonblock(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); + return ossl_start_ssl(self, SSL_accept, "SSL_accept"); } static VALUE -ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) +ossl_ssl_read_nonblock(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"); @@ -2026,224 +1870,112 @@ 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); + int saved_errno = errno_mapped(); + rb_str_unlocktmp(str); - 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_nonblock(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; + int num, nwritten; VALUE cb_state; GetSSL(self, ssl); if (!ssl_started(ssl)) rb_raise(eSSLError, "SSL session is not started yet"); - + StringValue(str); VALUE io = rb_attr_get(self, id_i_io); GetOpenFile(io, fptr); +#ifdef __APPLE__ + retry: +#endif /* SSL_write(3ssl) manpage states num == 0 is undefined */ 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; -#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); + 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"); + } } /* @@ -3167,14 +2899,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, "__connect_nonblock", ossl_ssl_connect_nonblock, 0); + rb_define_private_method(cSSLSocket, "__accept_nonblock", ossl_ssl_accept_nonblock, 0); + rb_define_private_method(cSSLSocket, "__sysread_nonblock", ossl_ssl_read_nonblock, 2); + rb_define_private_method(cSSLSocket, "__syswrite_nonblock", ossl_ssl_write_nonblock, 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); @@ -3352,8 +3080,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 dccc11a55..ac273d0f7 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -360,6 +360,210 @@ def sysclose 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 with_timeout(timeout) + # IO#timeout= was added in Ruby 3.2 + timeout ||= self.timeout if IO.method_defined?(:timeout) + if timeout.nil? + while true + yield + end + else + remaining = timeout + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + while remaining >= 0 + yield remaining + now = Process.clock_gettime(Process::CLOCK_MONOTONIC) + remaining = timeout - (now - start) + end + end + raise IO_TimeoutError, "user specified timeout for SSL handshake" + end + + # :call-seq: + # ssl.connect(timeout: nil) -> self + # + # Initiates an SSL/TLS handshake with a server. + # + # If _timeout_ is specified, and if the handshake does not complete + # within _timeout_ seconds, IO::TimeoutError is raised. + def connect(timeout: nil) + with_timeout(timeout) do |remaining| + case ret = __connect_nonblock + when :wait_readable + wait_readable(remaining) + when :wait_writable + wait_writable(remaining) + 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 = __connect_nonblock + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.accept(timeout: nil) -> self + # + # Waits for a SSL/TLS client to initiate a handshake. + # + # If _timeout_ is specified, and if the handshake does not complete + # within _timeout_ seconds, IO::TimeoutError is raised. + def accept(timeout: nil) + with_timeout(timeout) do |remaining| + case ret = __accept_nonblock + when :wait_readable + wait_readable(remaining) + when :wait_writable + wait_writable(remaining) + 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 = __accept_nonblock + 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) + while true + case ret = __sysread_nonblock(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. + def sysread_nonblock(length, buffer = nil, exception: true) + ret = __sysread_nonblock(length, buffer) + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.syswrite(string) -> Integer + # + # Writes _string_ to the SSL connection. + def syswrite(string) + while true + case ret = __syswrite_nonblock(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. + def syswrite_nonblock(string, exception: true) + ret = __syswrite_nonblock(string) + check_nonblock(ret) if exception + ret + end + # call-seq: # ssl.post_connection_check(hostname) -> true # diff --git a/test/openssl/test_pair.rb b/test/openssl/test_pair.rb index 10942191d..8cb1060e4 100644 --- a/test/openssl/test_pair.rb +++ b/test/openssl/test_pair.rb @@ -2,35 +2,34 @@ require_relative 'utils' require_relative 'ut_eof' -if defined?(OpenSSL::SSL) +return unless defined?(OpenSSL::SSL) -module OpenSSL::SSLPairM - def setup +module OpenSSL::SSLPair + def ssl_pair svr_dn = OpenSSL::X509::Name.parse("/DC=org/DC=ruby-lang/CN=localhost") ee_exts = [ ["keyUsage", "keyEncipherment,digitalSignature", true], ] - @svr_key = OpenSSL::TestUtils::Fixtures.pkey("rsa-1") - @svr_cert = issue_cert(svr_dn, @svr_key, 1, ee_exts, nil, nil) - end + svr_key = OpenSSL::TestUtils::Fixtures.pkey("rsa-1") + svr_cert = issue_cert(svr_dn, svr_key, 1, ee_exts, nil, nil) - def ssl_pair host = "127.0.0.1" - tcps = create_tcp_server(host, 0) - port = tcps.connect_address.ip_port + svr = TCPServer.new(host, 0) + svr.setsockopt(:TCP, :NODELAY, 1) + port = svr.connect_address.ip_port + tcps = nil th = Thread.new { + tcps = svr.accept sctx = OpenSSL::SSL::SSLContext.new - sctx.cert = @svr_cert - sctx.key = @svr_key - sctx.options |= OpenSSL::SSL::OP_NO_COMPRESSION - ssls = OpenSSL::SSL::SSLServer.new(tcps, sctx) - ns = ssls.accept - ssls.close - ns + sctx.add_certificate(svr_cert, svr_key) + ssl = OpenSSL::SSL::SSLSocket.new(tcps, sctx) + ssl.accept + ssl } - tcpc = create_tcp_client(host, port) + tcpc = TCPSocket.new(host, port) + tcpc.setsockopt(:TCP, :NODELAY, 1) c = OpenSSL::SSL::SSLSocket.new(tcpc) c.connect s = th.value @@ -39,57 +38,7 @@ def ssl_pair ensure tcpc&.close tcps&.close - s&.close - end -end - -module OpenSSL::SSLPair - include OpenSSL::SSLPairM - - def create_tcp_server(host, port) - TCPServer.new(host, port) - end - - def create_tcp_client(host, port) - TCPSocket.new(host, port) - end -end - -module OpenSSL::SSLPairLowlevelSocket - include OpenSSL::SSLPairM - - def create_tcp_server(host, port) - Addrinfo.tcp(host, port).listen - end - - def create_tcp_client(host, port) - Addrinfo.tcp(host, port).connect - end -end - -module OpenSSL::TestEOF1M - def open_file(content) - ssl_pair { |s1, s2| - begin - th = Thread.new { s2 << content; s2.close } - yield s1 - ensure - th&.join - end - } - end -end - -module OpenSSL::TestEOF2M - def open_file(content) - ssl_pair { |s1, s2| - begin - th = Thread.new { s1 << content; s1.close } - yield s2 - ensure - th&.join - end - } + svr&.close end end @@ -97,28 +46,36 @@ module OpenSSL::TestPairM def test_getc ssl_pair {|s1, s2| s1 << "a" + s1.close assert_equal(?a, s2.getc) + assert_nil(s2.getc) } end def test_getbyte ssl_pair {|s1, s2| s1 << "a" + s1.close assert_equal(97, s2.getbyte) + assert_nil(s2.getbyte) } end - def test_readbyte + def test_readchar ssl_pair {|s1, s2| s1 << "b" - assert_equal(98, s2.readbyte) + s1.close + assert_equal("b", s2.readchar) + assert_raise(EOFError) { s2.readchar } } end - def test_readbyte_eof + def test_readbyte ssl_pair {|s1, s2| - s2.close - assert_raise(EOFError) { s1.readbyte } + s1 << "b" + s1.close + assert_equal(98, s2.readbyte) + assert_raise(EOFError) { s2.readbyte } } end @@ -216,6 +173,25 @@ def test_multibyte_read_write } end + def test_sysread_and_syswrite + ssl_pair {|s1, s2| + str = "x" * 100 + "\n" + s1.syswrite(str) + newstr = s2.sysread(str.bytesize) + assert_equal(str, newstr) + + buf = String.new + s1.syswrite(str) + assert_same(buf, s2.sysread(str.size, buf)) + assert_equal(str, buf) + + obj = Object.new + obj.define_singleton_method(:to_str) { str } + s1.syswrite(obj) + assert_equal(str, s2.sysread(str.bytesize)) + } + end + def test_read_nonblock ssl_pair {|s1, s2| err = nil @@ -393,151 +369,33 @@ def test_write_multiple_arguments } end - def test_partial_tls_record_read_nonblock + def test_copy_stream ssl_pair { |s1, s2| - # the beginning of a TLS record - s1.io.write("\x17") - # should raise a IO::WaitReadable since a full TLS record is not available - # for reading - assert_raise(IO::WaitReadable) { s2.read_nonblock(1) } + IO.pipe do |r, w| + str = "hello world\n" + w.write(str) + IO.copy_stream(r, s1, str.bytesize) + IO.copy_stream(s2, w, str.bytesize) + assert_equal(str, r.read(str.bytesize)) + end } end - def tcp_pair - host = "127.0.0.1" - serv = TCPServer.new(host, 0) - port = serv.connect_address.ip_port - sock1 = TCPSocket.new(host, port) - sock2 = serv.accept - serv.close - [sock1, sock2] - ensure - serv.close if serv && !serv.closed? - end - - def test_connect_accept_nonblock_no_exception - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.cert = @svr_cert - ctx2.key = @svr_key - - sock1, sock2 = tcp_pair - - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - accepted = s2.accept_nonblock(exception: false) - assert_equal :wait_readable, accepted - - ctx1 = OpenSSL::SSL::SSLContext.new - s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - th = Thread.new do - rets = [] - begin - rv = s1.connect_nonblock(exception: false) - rets << rv - case rv - when :wait_writable - IO.select(nil, [s1], nil, 5) - when :wait_readable - IO.select([s1], nil, nil, 5) - end - end until rv == s1 - rets - end - - until th.join(0.01) - accepted = s2.accept_nonblock(exception: false) - assert_include([s2, :wait_readable, :wait_writable ], accepted) - end - - rets = th.value - assert_instance_of Array, rets - rets.each do |rv| - assert_include([s1, :wait_readable, :wait_writable ], rv) - end - ensure - th.join if th - s1.close if s1 - s2.close if s2 - sock1.close if sock1 - sock2.close if sock2 - accepted.close if accepted.respond_to?(:close) - end - - def test_connect_accept_nonblock - ctx = OpenSSL::SSL::SSLContext.new - ctx.cert = @svr_cert - ctx.key = @svr_key - - sock1, sock2 = tcp_pair - - th = Thread.new { - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx) - 5.times { - begin - break s2.accept_nonblock - rescue IO::WaitReadable - IO.select([s2], nil, nil, 1) - rescue IO::WaitWritable - IO.select(nil, [s2], nil, 1) - end - sleep 0.2 - } - } - - s1 = OpenSSL::SSL::SSLSocket.new(sock1) - 5.times { - begin - break s1.connect_nonblock - rescue IO::WaitReadable - IO.select([s1], nil, nil, 1) - rescue IO::WaitWritable - IO.select(nil, [s1], nil, 1) - end - sleep 0.2 + def test_close_write + ssl_pair { |s1, s2| + message = "abc"*1024 + s1.write(message) + s1.close_write + assert_equal(message, s2.read) + s2.write(message) + s2.close_write + assert_equal(message, s1.read) } - - s2 = th.value - - s1.print "a\ndef" - assert_equal("a\n", s2.gets) - ensure - sock1&.close - sock2&.close - th&.join end end -class OpenSSL::TestEOF1 < OpenSSL::TestCase - include OpenSSL::TestEOF +class OpenSSL::TestSSLPair < OpenSSL::TestCase include OpenSSL::SSLPair - include OpenSSL::TestEOF1M -end - -class OpenSSL::TestEOF1LowlevelSocket < OpenSSL::TestCase - include OpenSSL::TestEOF - include OpenSSL::SSLPairLowlevelSocket - include OpenSSL::TestEOF1M -end - -class OpenSSL::TestEOF2 < OpenSSL::TestCase - include OpenSSL::TestEOF - include OpenSSL::SSLPair - include OpenSSL::TestEOF2M -end - -class OpenSSL::TestEOF2LowlevelSocket < OpenSSL::TestCase - include OpenSSL::TestEOF - include OpenSSL::SSLPairLowlevelSocket - include OpenSSL::TestEOF2M -end - -class OpenSSL::TestPair < OpenSSL::TestCase - include OpenSSL::SSLPair - include OpenSSL::TestPairM -end - -class OpenSSL::TestPairLowlevelSocket < OpenSSL::TestCase - include OpenSSL::SSLPairLowlevelSocket include OpenSSL::TestPairM -end - + include OpenSSL::TestEOF end diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 914b80623..13d458911 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -75,24 +75,23 @@ def test_ctx_options_config end def test_ssl_with_server_cert - ctx_proc = -> ctx { - ctx.cert = @svr_cert - ctx.key = @svr_key - ctx.extra_chain_cert = [@ca_cert] - } - server_proc = -> (ctx, ssl) { + sctx = OpenSSL::SSL::SSLContext.new + sctx.cert = @svr_cert + sctx.key = @svr_key + sctx.extra_chain_cert = [@ca_cert] + + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept assert_equal @svr_cert.to_der, ssl.cert.to_der assert_equal nil, ssl.peer_cert - - readwrite_loop(ctx, ssl) - } - start_server(ctx_proc: ctx_proc, server_proc: server_proc) { |port| + readwrite_loop(ssl) + end + start_server_proc(server_proc) { |port| begin sock = TCPSocket.new("127.0.0.1", port) ctx = OpenSSL::SSL::SSLContext.new ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) ssl.connect - assert_equal sock, ssl.io assert_equal nil, ssl.cert assert_equal @svr_cert.to_der, ssl.peer_cert.to_der @@ -108,6 +107,141 @@ def test_ssl_with_server_cert } end + def test_connect_accept_nonblock_no_exception + server_proc = proc do |sock| + s2 = OpenSSL::SSL::SSLSocket.new(sock, make_server_context) + accepted = s2.accept_nonblock(exception: false) + assert_equal(:wait_readable, accepted) + loop do + rv = s2.accept_nonblock(exception: false) + case rv + when :wait_readable + IO.select([s2], nil, nil, 1) + when :wait_writable + IO.select(nil, [s2], nil, 1) + else + assert_same(s2, rv) + break + end + end + assert_equal("abc\n", s2.gets) + s2.puts("abc") + s2.close + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + s1 = OpenSSL::SSL::SSLSocket.new(sock) + loop do + rv = s1.connect_nonblock(exception: false) + case rv + when :wait_readable + IO.select([s1], nil, nil, 1) + when :wait_writable + IO.select(nil, [s1], nil, 1) + else + assert_same(s1, rv) + break + end + end + s1.puts("abc") + assert_equal("abc\n", s1.gets) + ensure + sock&.close + end + end + + def test_connect_accept_nonblock + server_proc = proc do |sock| + s2 = OpenSSL::SSL::SSLSocket.new(sock, make_server_context) + rv = 5.times { + begin + break s2.accept_nonblock + rescue IO::WaitReadable + IO.select([s2], nil, nil, 1) + rescue IO::WaitWritable + IO.select(nil, [s2], nil, 1) + end + } + assert_same(s2, rv) + assert_equal("a\n", s2.gets) + s2.puts("b") + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + s1 = OpenSSL::SSL::SSLSocket.new(sock) + rv = 5.times { + begin + break s1.connect_nonblock + rescue IO::WaitReadable + IO.select([s1], nil, nil, 1) + rescue IO::WaitWritable + IO.select(nil, [s1], nil, 1) + end + } + assert_same(s1, rv) + s1.print "a\ndef" + assert_equal("b\n", s1.gets) + ensure + sock&.close + end + end + + def test_connect_timeout + timeout_error = defined?(IO::TimeoutError) ? IO::TimeoutError : IOError + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + server_proc = proc do |sock| + # TLS 1.2 handshake takes 2 RTTs + ctx = make_server_context + ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) + + case (sleep 0.1; ssl.accept_nonblock(exception: false)) + when :wait_readable then ssl.wait_readable + when :wait_writable then ssl.wait_writable + else break + end while true + readwrite_loop(ssl) + rescue OpenSSL::SSL::SSLError, SystemCallError + end + start_server_proc(server_proc) do |port| + th = [] + th << Thread.new do + sock = TCPSocket.new("127.0.0.1", port) + sock.setsockopt(:TCP, :NODELAY, 1) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + assert_raise(timeout_error) { ssl.connect(timeout: 0.05) } + taken = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + assert_operator(taken, :<, 0.2) + ensure + sock.close + end + th << Thread.new do + sock = TCPSocket.new("127.0.0.1", port) + sock.setsockopt(:TCP, :NODELAY, 1) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.connect(timeout: 1) + taken = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + assert_operator(taken, :>=, 0.2) + ensure + sock.close + end + assert_join_threads(th) + end + end + + def test_low_level_socket + start_server do |port| + sock = Socket.tcp("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.connect + ssl.puts("abc") + assert_equal("abc\n", ssl.gets) + ensure + ssl&.close + sock&.close + end + end + def test_socket_open start_server { |port| begin @@ -158,37 +292,11 @@ def test_socket_open_with_local_address_port_context } end - def test_socket_close_write - server_proc = proc do |ctx, ssl| - message = ssl.read - ssl.write(message) - ssl.close_write - ensure - ssl.close - end - - start_server(server_proc: server_proc) do |port| - ctx = OpenSSL::SSL::SSLContext.new - ssl = OpenSSL::SSL::SSLSocket.open("127.0.0.1", port, context: ctx) - ssl.sync_close = true - ssl.connect - - message = "abc"*1024 - ssl.write message - ssl.close_write - assert_equal message, ssl.read - ensure - ssl&.close - end - end - def test_add_certificate - ctx_proc = -> ctx { - # Unset values set by start_server - ctx.cert = ctx.key = ctx.extra_chain_cert = nil - ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA + + start_server(sctx) do |port| server_connect(port) { |ssl| assert_equal @svr_cert.subject, ssl.peer_cert.subject assert_equal [@svr_cert.subject, @ca_cert.subject], @@ -215,13 +323,11 @@ def test_add_certificate_multiple_certs ecdsa_dn = OpenSSL::X509::Name.parse_rfc2253("CN=localhost2") ecdsa_cert = issue_cert(ecdsa_dn, ecdsa_key, 456, exts, ca2_cert, ca2_key) - ctx_proc = -> ctx { - # Unset values set by start_server - ctx.cert = ctx.key = ctx.extra_chain_cert = nil - ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA - ctx.add_certificate(ecdsa_cert, ecdsa_key, [ca2_cert]) - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA + sctx.add_certificate(ecdsa_cert, ecdsa_key, [ca2_cert]) + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 # TODO: We need this to force certificate type ctx.ciphers = "aECDSA" @@ -253,14 +359,12 @@ def test_extra_chain_cert_auto_chain # AWS-LC enables SSL_MODE_NO_AUTO_CHAIN by default unless aws_lc? - ctx_proc = -> ctx { - # Sanity check: start_server won't set extra_chain_cert - assert_nil ctx.extra_chain_cert - ctx.cert_store = OpenSSL::X509::Store.new.tap { |store| - store.add_cert(@ca_cert) - } + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key) # no extra certs + sctx.cert_store = OpenSSL::X509::Store.new.tap { |store| + store.add_cert(@ca_cert) } - start_server(ctx_proc: ctx_proc) { |port| + start_server(sctx) { |port| server_connect(port) { |ssl| ssl.puts "abc"; assert_equal "abc\n", ssl.gets assert_equal @svr_cert.to_der, ssl.peer_cert.to_der @@ -270,27 +374,6 @@ def test_extra_chain_cert_auto_chain end end - def test_sysread_and_syswrite - start_server { |port| - server_connect(port) { |ssl| - str = +("x" * 100 + "\n") - ssl.syswrite(str) - newstr = ssl.sysread(str.bytesize) - assert_equal(str, newstr) - - buf = String.new - ssl.syswrite(str) - assert_same buf, ssl.sysread(str.size, buf) - assert_equal(str, buf) - - obj = Object.new - obj.define_singleton_method(:to_str) { str } - ssl.syswrite(obj) - assert_equal(str, ssl.sysread(str.bytesize)) - } - } - end - def test_read_with_timeout omit "does not support timeout" unless IO.method_defined?(:timeout) @@ -314,30 +397,30 @@ def test_read_with_timeout end end - def test_getbyte - start_server { |port| - server_connect(port) { |ssl| - str = +("x" * 100 + "\n") - ssl.syswrite(str) - newstr = str.bytesize.times.map { |i| - ssl.getbyte - }.pack("C*") - assert_equal(str, newstr) - } - } - end + def test_partial_tls_record_read_nonblock + written = Thread::Queue.new + read = Thread::Queue.new + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, make_server_context).accept + str = ssl.gets + ssl.puts(str) - def test_readbyte - start_server { |port| - server_connect(port) { |ssl| - str = +("x" * 100 + "\n") - ssl.syswrite(str) - newstr = str.bytesize.times.map { |i| - ssl.readbyte - }.pack("C*") - assert_equal(str, newstr) - } - } + # the beginning of a TLS record + ssl.io.write("\x17") + written << :written + read.pop + end + start_server_proc(server_proc) do |port| + server_connect(port) do |ssl| + ssl.puts("abc") + assert_equal("abc\n", ssl.gets) + written.pop + # should raise a IO::WaitReadable since a full TLS record is not available + # for reading + assert_raise(IO::WaitReadable) { ssl.send(:sysread_nonblock, 1) } + read << :done + end + end end def test_sync_close @@ -383,20 +466,6 @@ def test_sync_close_initialize_opt end end - def test_copy_stream - start_server do |port| - server_connect(port) do |ssl| - IO.pipe do |r, w| - str = "hello world\n" - w.write(str) - IO.copy_stream(r, ssl, str.bytesize) - IO.copy_stream(ssl, w, str.bytesize) - assert_equal str, r.read(str.bytesize) - end - end - end - end - def test_verify_mode_default ctx = OpenSSL::SSL::SSLContext.new assert_equal OpenSSL::SSL::VERIFY_NONE, ctx.verify_mode @@ -436,36 +505,44 @@ def test_verify_mode_server_cert def test_verify_mode_client_cert_required # Optional, client certificate not supplied - vflag = OpenSSL::SSL::VERIFY_PEER - accept_proc = -> ssl { + sctx = make_server_context + sctx.verify_mode = OpenSSL::SSL::VERIFY_PEER + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept assert_equal nil, ssl.peer_cert - } - start_server(verify_mode: vflag, accept_proc: accept_proc) { |port| + readwrite_loop(ssl) + end + start_server_proc(server_proc) { |port| assert_nothing_raised { server_connect(port) { |ssl| ssl.puts("abc"); ssl.gets } } } # Required, client certificate not supplied - vflag = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - start_server(verify_mode: vflag, ignore_listener_error: true) { |port| - assert_handshake_error { + sctx = make_server_context + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + start_server(sctx, ignore_listener_error: true) { |port| + # TLS 1.3 alert: certificate_required(116) + assert_raise_with_message(OpenSSL::SSL::SSLError, /alert number 116/) { server_connect(port) { |ssl| ssl.puts("abc"); ssl.gets } } } end def test_client_auth_success - vflag = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - ctx_proc = proc { |ctx| - store = OpenSSL::X509::Store.new - store.add_cert(@ca_cert) - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - ctx.cert_store = store - # LibreSSL doesn't support client_cert_cb in TLS 1.3 - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION if libressl? - } - start_server(verify_mode: vflag, ctx_proc: ctx_proc) { |port| + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + + sctx = make_server_context + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + sctx.cert_store = store + # LibreSSL doesn't support client_cert_cb in TLS 1.3 + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION if libressl? + + start_server(sctx) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.key = @cli_key ctx.cert = @cli_cert @@ -491,8 +568,11 @@ def test_client_auth_success end def test_client_cert_cb_ignore_error - vflag = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - start_server(verify_mode: vflag, ignore_listener_error: true) do |port| + sctx = make_server_context + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + + start_server(sctx, ignore_listener_error: true) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.client_cert_cb = -> ssl { raise "exception in client_cert_cb must be suppressed" @@ -500,7 +580,7 @@ def test_client_cert_cb_ignore_error # 1. Exception in client_cert_cb is suppressed # 2. No client certificate will be sent to the server # 3. SSL_VERIFY_FAIL_IF_NO_PEER_CERT causes the handshake to fail - assert_handshake_error { + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) { |ssl| ssl.puts("abc"); ssl.gets } } end @@ -509,16 +589,16 @@ def test_client_cert_cb_ignore_error def test_client_ca pend "LibreSSL doesn't support certificate_authorities" if libressl? - ctx_proc = Proc.new do |ctx| - store = OpenSSL::X509::Store.new - store.add_cert(@ca_cert) - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - ctx.cert_store = store - ctx.client_ca = [@ca_cert] - end + sctx = make_server_context + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + sctx.cert_store = store + sctx.client_ca = [@ca_cert] - vflag = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - start_server(verify_mode: vflag, ctx_proc: ctx_proc) { |port| + start_server(sctx) { |port| ctx = OpenSSL::SSL::SSLContext.new client_ca_from_server = nil ctx.client_cert_cb = Proc.new do |sslconn| @@ -680,13 +760,14 @@ def test_finished_messages client_finished = nil client_peer_finished = nil - start_server(accept_proc: proc { |server| - server_finished = server.finished_message - server_peer_finished = server.peer_finished_message - }) { |port| - ctx = OpenSSL::SSL::SSLContext.new - ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE - server_connect(port, ctx) { |ssl| + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, make_server_context).accept + server_finished = ssl.finished_message + server_peer_finished = ssl.peer_finished_message + readwrite_loop(ssl) + end + start_server_proc(server_proc) { |port| + server_connect(port) { |ssl| ssl.puts "abc"; ssl.gets client_finished = ssl.finished_message @@ -717,14 +798,13 @@ def test_post_connect_check_with_anon_ciphers omit_on_fips omit "AWS-LC does not support DHE ciphersuites" if aws_lc? - ctx_proc = -> ctx { - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.ciphers = "aNULL" - ctx.tmp_dh = Fixtures.pkey("dh-1") - ctx.security_level = 0 - } + sctx = OpenSSL::SSL::SSLContext.new + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.ciphers = "aNULL" + sctx.tmp_dh = Fixtures.pkey("dh-1") + sctx.security_level = 0 - start_server(ctx_proc: ctx_proc) { |port| + start_server(sctx) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION ctx.ciphers = "aNULL" @@ -991,14 +1071,6 @@ def create_null_byte_SAN_certificate(critical = false) cert end - def socketpair - if defined? UNIXSocket - UNIXSocket.pair - else - Socket.pair(Socket::AF_INET, Socket::SOCK_STREAM, 0) - end - end - def test_keylog_cb omit "Keylog callback is not supported" if libressl? @@ -1050,19 +1122,18 @@ def test_tlsext_hostname fooctx.cert = @cli_cert fooctx.key = @cli_key - ctx_proc = proc { |ctx| - ctx.servername_cb = proc { |ssl, servername| - case servername - when "foo.example.com" - fooctx - when "bar.example.com" - nil - else - raise "unreachable" - end - } + sctx = make_server_context + sctx.servername_cb = proc { |ssl, servername| + case servername + when "foo.example.com" + fooctx + when "bar.example.com" + nil + else + raise "unreachable" + end } - start_server(ctx_proc: ctx_proc) do |port| + start_server(sctx) do |port| sock = TCPSocket.new("127.0.0.1", port) begin ssl = OpenSSL::SSL::SSLSocket.new(sock) @@ -1093,84 +1164,54 @@ def test_tlsext_hostname end def test_servername_cb_exception - sock1, sock2 = socketpair - - t = Thread.new { - s1 = OpenSSL::SSL::SSLSocket.new(sock1) - s1.hostname = "localhost" + server_proc = proc do |sock| + sctx = make_server_context + sctx.servername_cb = lambda { |args| raise RuntimeError, "foo" } + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx) + assert_raise_with_message(RuntimeError, "foo") { ssl.accept } + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.hostname = "example.org" assert_raise_with_message(OpenSSL::SSL::SSLError, /unrecognized.name/i) { - s1.connect + ssl.connect } - } - - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.servername_cb = lambda { |args| raise RuntimeError, "foo" } - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - assert_raise_with_message(RuntimeError, "foo") { s2.accept } - assert t.join - ensure - sock1.close - sock2.close - t.kill.join + ensure + sock&.close + end end def test_servername_cb_raises_an_exception_on_unknown_objects - sock1, sock2 = socketpair - - t = Thread.new { - s1 = OpenSSL::SSL::SSLSocket.new(sock1) - s1.hostname = "localhost" - assert_raise(OpenSSL::SSL::SSLError) { s1.connect } - } - - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.servername_cb = lambda { |args| Object.new } - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - assert_raise(ArgumentError) { s2.accept } - assert t.join - ensure - sock1.close - sock2.close - t.kill.join - end - - def test_accept_errors_include_peeraddr - context = OpenSSL::SSL::SSLContext.new - context.cert = @svr_cert - context.key = @svr_key - - server = TCPServer.new("127.0.0.1", 0) - port = server.connect_address.ip_port - - ssl_server = OpenSSL::SSL::SSLServer.new(server, context) - - t = Thread.new do - assert_raise_with_message(OpenSSL::SSL::SSLError, /peeraddr=127\.0\.0\.1/) do - ssl_server.accept - end + server_proc = proc do |sock| + sctx = make_server_context + sctx.servername_cb = lambda { |args| Object.new } + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx) + assert_raise(ArgumentError) { ssl.accept } + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + ssl.hostname = "example.org" + assert_raise_with_message(OpenSSL::SSL::SSLError, /unrecognized.name/i) { + ssl.connect + } + ensure + sock&.close end - - sock = TCPSocket.new("127.0.0.1", port) - sock << "\x00" * 1024 - - assert t.join - ensure - sock&.close - server.close end def test_verify_hostname_on_connect - ctx_proc = proc { |ctx| - exts = [ - ["keyUsage", "keyEncipherment,digitalSignature", true], - ["subjectAltName", "DNS:a.example.com,DNS:*.b.example.com," \ - "DNS:c*.example.com,DNS:d.*.example.com"], - ] - ctx.cert = issue_cert(@svr, @svr_key, 4, exts, @ca_cert, @ca_key) - ctx.key = @svr_key - } + exts = [ + ["keyUsage", "keyEncipherment,digitalSignature", true], + ["subjectAltName", "DNS:a.example.com,DNS:*.b.example.com," \ + "DNS:c*.example.com,DNS:d.*.example.com"], + ] + cert = issue_cert(@svr, @svr_key, 4, exts, @ca_cert, @ca_key) + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(cert, @svr_key) - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + start_server(sctx, ignore_listener_error: true) do |port| ctx = OpenSSL::SSL::SSLContext.new assert_equal false, ctx.verify_hostname ctx.verify_hostname = true @@ -1206,16 +1247,15 @@ def test_verify_hostname_on_connect end def test_verify_hostname_failure_error_code - ctx_proc = proc { |ctx| - exts = [ - ["keyUsage", "keyEncipherment,digitalSignature", true], - ["subjectAltName", "DNS:a.example.com"], - ] - ctx.cert = issue_cert(@svr, @svr_key, 4, exts, @ca_cert, @ca_key) - ctx.key = @svr_key - } + exts = [ + ["keyUsage", "keyEncipherment,digitalSignature", true], + ["subjectAltName", "DNS:a.example.com"], + ] + cert = issue_cert(@svr, @svr_key, 4, exts, @ca_cert, @ca_key) + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(cert, @svr_key) - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + start_server(sctx, ignore_listener_error: true) do |port| verify_callback_ok = verify_callback_err = nil ctx = OpenSSL::SSL::SSLContext.new @@ -1242,6 +1282,27 @@ def test_verify_hostname_failure_error_code end end + def test_connect_systemcallerror + # SSL_connect() should fail with SSL_ERROR_SYSCALL and errno should be + # kept intact from the underlying recv(2)/send(2). + pend "AWS-LC does not preserve errno on SSL_ERROR_SYSCALL" if aws_lc? + + server_proc = proc do |sock| + sock.setsockopt(:SOCKET, :LINGER, [1, 0].pack("ii")) + sock.read(1) + sock.close + end + start_server_proc(server_proc) do |port| + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock) + assert_raise(Errno::ECONNRESET, Errno::EPIPE) { + ssl.connect + } + ensure + sock&.close + end + end + def test_connect_certificate_verify_failed_exception_message start_server(ignore_listener_error: true) { |port| ctx = OpenSSL::SSL::SSLContext.new @@ -1251,12 +1312,13 @@ def test_connect_certificate_verify_failed_exception_message } } - ctx_proc = proc { |ctx| - now = Time.now - ctx.cert = issue_cert(@svr, @svr_key, 30, [], @ca_cert, @ca_key, - not_before: now - 7200, not_after: now - 3600) - } - start_server(ignore_listener_error: true, ctx_proc: ctx_proc) { |port| + now = Time.now + cert = issue_cert(@svr, @svr_key, 30, [], @ca_cert, @ca_key, + not_before: now - 7200, not_after: now - 3600) + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(cert, @svr_key) + + start_server(sctx, ignore_listener_error: true) { |port| store = OpenSSL::X509::Store.new store.add_cert(@ca_cert) ctx = OpenSSL::SSL::SSLContext.new @@ -1267,6 +1329,16 @@ def test_connect_certificate_verify_failed_exception_message } end + def test_connect_exception_message_include_peeraddr + start_server(ignore_listener_error: true) do |port| + ctx = OpenSSL::SSL::SSLContext.new + ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER + assert_raise_with_message(OpenSSL::SSL::SSLError, /peeraddr=127\.0\.0\.1/) do + server_connect(port, ctx) { } + end + end + end + def check_supported_protocol_versions possible_versions = [ OpenSSL::SSL::SSL3_VERSION, @@ -1275,16 +1347,16 @@ def check_supported_protocol_versions OpenSSL::SSL::TLS1_2_VERSION, OpenSSL::SSL::TLS1_3_VERSION, ] - supported = [] - ctx_proc = proc { |ctx| - # The default security level is 1 in OpenSSL <= 3.1, 2 in OpenSSL >= 3.2 - # In OpenSSL >= 3.0, TLS 1.1 or older is disabled at level 1 - ctx.security_level = 0 - # Explicitly reset them to avoid influenced by OPENSSL_CONF - ctx.min_version = ctx.max_version = nil - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + + sctx = make_server_context + # The default security level is 1 in OpenSSL <= 3.1, 2 in OpenSSL >= 3.2 + # In OpenSSL >= 3.0, TLS 1.1 or older is disabled at level 1 + sctx.security_level = 0 + # Explicitly reset them to avoid influenced by OPENSSL_CONF + sctx.min_version = sctx.max_version = nil + + start_server(sctx, ignore_listener_error: true) do |port| possible_versions.each do |ver| ctx = OpenSSL::SSL::SSLContext.new ctx.security_level = 0 @@ -1293,7 +1365,7 @@ def check_supported_protocol_versions ssl.puts "abc"; assert_equal "abc\n", ssl.gets } supported << ver - rescue OpenSSL::SSL::SSLError, Errno::ECONNRESET + rescue OpenSSL::SSL::SSLError end end @@ -1306,20 +1378,19 @@ def check_supported_protocol_versions def test_set_params_min_version supported = check_supported_protocol_versions - store = OpenSSL::X509::Store.new - store.add_cert(@ca_cert) + return unless supported.include?(OpenSSL::SSL::SSL3_VERSION) - if supported.include?(OpenSSL::SSL::SSL3_VERSION) - # SSLContext#set_params properly disables SSL 3.0 by default - ctx_proc = proc { |ctx| - ctx.min_version = ctx.max_version = OpenSSL::SSL::SSL3_VERSION - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| - ctx = OpenSSL::SSL::SSLContext.new - ctx.set_params(cert_store: store, verify_hostname: false) - assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) } - } - end + # SSLContext#set_params properly disables SSL 3.0 by default + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::SSL3_VERSION + + start_server(sctx, ignore_listener_error: true) { |port| + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + ctx = OpenSSL::SSL::SSLContext.new + ctx.set_params(cert_store: store, verify_hostname: false) + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx) } + } end def test_minmax_version @@ -1338,11 +1409,11 @@ def test_minmax_version # Server enables a single version supported.each do |ver| - ctx_proc = proc { |ctx| - ctx.security_level = 0 - ctx.min_version = ctx.max_version = ver - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.security_level = 0 + sctx.min_version = sctx.max_version = ver + + start_server(sctx, ignore_listener_error: true) { |port| supported.each do |cver| # Client enables a single version ctx1 = OpenSSL::SSL::SSLContext.new @@ -1391,11 +1462,11 @@ def test_minmax_version # Server sets min_version (earliest is disabled) sver = supported[1] - ctx_proc = proc { |ctx| - ctx.security_level = 0 - ctx.min_version = sver - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.security_level = 0 + sctx.min_version = sver + + start_server(sctx, ignore_listener_error: true) { |port| supported.each do |cver| # Client sets min_version ctx1 = OpenSSL::SSL::SSLContext.new @@ -1425,12 +1496,12 @@ def test_minmax_version # Server sets max_version (latest is disabled) sver = supported[-2] - ctx_proc = proc { |ctx| - ctx.security_level = 0 - ctx.min_version = 0 - ctx.max_version = sver - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.security_level = 0 + sctx.min_version = 0 + sctx.max_version = sver + + start_server(sctx, ignore_listener_error: true) { |port| supported.each do |cver| # Client sets min_version ctx1 = OpenSSL::SSL::SSLContext.new @@ -1520,10 +1591,9 @@ def test_respect_system_default_min EOF f.close - ctx_proc = proc { |ctx| - ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + start_server(sctx, ignore_listener_error: true) do |port| assert_separately([{ "OPENSSL_CONF" => f.path }, "-ropenssl", "-", port.to_s], <<~"end;") sock = TCPSocket.new("127.0.0.1", ARGV[0].to_i) ctx = OpenSSL::SSL::SSLContext.new @@ -1536,10 +1606,9 @@ def test_respect_system_default_min end; end - ctx_proc = proc { |ctx| - ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::TLS1_3_VERSION + start_server(sctx, ignore_listener_error: true) do |port| assert_separately([{ "OPENSSL_CONF" => f.path }, "-ropenssl", "-", port.to_s], <<~"end;") sock = TCPSocket.new("127.0.0.1", ARGV[0].to_i) ctx = OpenSSL::SSL::SSLContext.new @@ -1566,12 +1635,11 @@ def test_options_disable_versions end # Server disables TLS 1.2 and earlier - ctx_proc = proc { |ctx| - ctx.options |= OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 | - OpenSSL::SSL::OP_NO_TLSv1 | OpenSSL::SSL::OP_NO_TLSv1_1 | - OpenSSL::SSL::OP_NO_TLSv1_2 - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.options |= OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 | + OpenSSL::SSL::OP_NO_TLSv1 | OpenSSL::SSL::OP_NO_TLSv1_1 | + OpenSSL::SSL::OP_NO_TLSv1_2 + start_server(sctx, ignore_listener_error: true) { |port| # Client only supports TLS 1.2 ctx1 = OpenSSL::SSL::SSLContext.new ctx1.min_version = ctx1.max_version = OpenSSL::SSL::TLS1_2_VERSION @@ -1584,10 +1652,9 @@ def test_options_disable_versions } # Server only supports TLS 1.2 - ctx_proc = proc { |ctx| - ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.min_version = sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + start_server(sctx, ignore_listener_error: true) { |port| # Client doesn't support TLS 1.2 ctx1 = OpenSSL::SSL::SSLContext.new ctx1.options |= OpenSSL::SSL::OP_NO_TLSv1_2 @@ -1613,9 +1680,13 @@ def test_ssl_methods_constant def test_renegotiation_cb num_handshakes = 0 - renegotiation_cb = Proc.new { |ssl| num_handshakes += 1 } - ctx_proc = Proc.new { |ctx| ctx.renegotiation_cb = renegotiation_cb } - start_server(ctx_proc: ctx_proc) { |port| + sctx = make_server_context + sctx.renegotiation_cb = -> ssl { + num_handshakes += 1 + assert_kind_of(OpenSSL::SSL::SSLSocket, ssl) + } + + start_server(sctx) { |port| server_connect(port) { |ssl| assert_equal(1, num_handshakes) ssl.puts "abc"; assert_equal "abc\n", ssl.gets @@ -1625,13 +1696,10 @@ def test_renegotiation_cb def test_alpn_protocol_selection_ary advertised = ["http/1.1", "spdy/2"] - ctx_proc = Proc.new { |ctx| - ctx.alpn_select_cb = -> (protocols) { - protocols.first - } - ctx.alpn_protocols = advertised - } - start_server(ctx_proc: ctx_proc) { |port| + sctx = make_server_context + sctx.alpn_select_cb = -> protocols { protocols.first } + + start_server(sctx) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.alpn_protocols = advertised server_connect(port, ctx) { |ssl| @@ -1642,38 +1710,30 @@ def test_alpn_protocol_selection_ary end def test_alpn_protocol_selection_cancel - sock1, sock2 = socketpair - - ctx1 = OpenSSL::SSL::SSLContext.new - ctx1.cert = @svr_cert - ctx1.key = @svr_key - ctx1.alpn_select_cb = -> (protocols) { nil } - ssl1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.alpn_protocols = ["http/1.1"] - ssl2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - - t = Thread.new { - ssl2.connect_nonblock(exception: false) - } - assert_raise_with_message(TypeError, /nil/) { ssl1.accept } - t.join - ensure - sock1&.close - sock2&.close - ssl1&.close - ssl2&.close - t&.kill - t&.join + server_proc = proc do |sock| + sctx = make_server_context + sctx.alpn_select_cb = -> (protocols) { nil } + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx) + assert_raise_with_message(TypeError, /nil/) { ssl.accept } + end + start_server_proc(server_proc) do |port| + ctx = OpenSSL::SSL::SSLContext.new + ctx.alpn_protocols = ["http/1.1"] + # no_application_protocol alert + assert_raise_with_message(OpenSSL::SSL::SSLError, /alert number 120/) { + server_connect(port, ctx) { } + } + end end def test_npn_protocol_selection_ary return unless OpenSSL::SSL::SSLContext.method_defined?(:npn_select_cb) advertised = ["http/1.1", "spdy/2"] - ctx_proc = proc { |ctx| ctx.npn_protocols = advertised } - start_server(ctx_proc: ctx_proc) { |port| + sctx = make_server_context + sctx.npn_protocols = advertised + + start_server(sctx) { |port| selector = lambda { |which| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 @@ -1695,8 +1755,10 @@ def advertised.each yield "http/1.1" yield "spdy/2" end - ctx_proc = Proc.new { |ctx| ctx.npn_protocols = advertised } - start_server(ctx_proc: ctx_proc) { |port| + sctx = make_server_context + sctx.npn_protocols = advertised + + start_server(sctx) { |port| selector = lambda { |selected, which| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 @@ -1713,8 +1775,10 @@ def advertised.each def test_npn_protocol_selection_cancel return unless OpenSSL::SSL::SSLContext.method_defined?(:npn_select_cb) - ctx_proc = Proc.new { |ctx| ctx.npn_protocols = ["http/1.1"] } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.npn_protocols = ["http/1.1"] + + start_server(sctx, ignore_listener_error: true) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 ctx.npn_select_cb = -> (protocols) { raise RuntimeError.new } @@ -1735,8 +1799,10 @@ def test_npn_advertised_protocol_too_long def test_npn_selected_protocol_too_long return unless OpenSSL::SSL::SSLContext.method_defined?(:npn_select_cb) - ctx_proc = Proc.new { |ctx| ctx.npn_protocols = ["http/1.1"] } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) { |port| + sctx = make_server_context + sctx.npn_protocols = ["http/1.1"] + + start_server(sctx, ignore_listener_error: true) { |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = :TLS1_2 ctx.npn_select_cb = -> (protocols) { "a" * 256 } @@ -1744,13 +1810,14 @@ def test_npn_selected_protocol_too_long } end - def readwrite_loop_safe(ctx, ssl) - readwrite_loop(ctx, ssl) - rescue OpenSSL::SSL::SSLError - end - def test_close_after_socket_close - start_server(server_proc: method(:readwrite_loop_safe)) { |port| + # The client closes the TCP socket without SSLSocket#stop + sctx = make_server_context + if defined?(OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF) + sctx.options |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF + end + + start_server(sctx) { |port| sock = TCPSocket.new("127.0.0.1", port) ssl = OpenSSL::SSL::SSLSocket.new(sock) ssl.connect @@ -1776,11 +1843,10 @@ def test_get_ephemeral_key omit_on_fips # kRSA - ctx_proc1 = proc { |ctx| - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.ciphers = "kRSA" - } - start_server(ctx_proc: ctx_proc1, ignore_listener_error: true) do |port| + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.ciphers = "kRSA" + start_server(sctx, ignore_listener_error: true) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION ctx.ciphers = "kRSA" @@ -1809,10 +1875,9 @@ def test_get_ephemeral_key end # ECDHE - ctx_proc3 = proc { |ctx| - ctx.groups = "P-256" - } - start_server(ctx_proc: ctx_proc3) do |port| + sctx = make_server_context + sctx.groups = "P-256" + start_server(sctx) do |port| server_connect(port) { |ssl| assert_instance_of OpenSSL::PKey::EC, ssl.tmp_key ssl.puts "abc"; assert_equal "abc\n", ssl.gets @@ -1836,12 +1901,11 @@ def test_fallback_scsv server_connect(port, ctx) end - ctx_proc = proc { |ctx| - ctx.security_level = 0 - ctx.min_version = 0 - ctx.max_version = OpenSSL::SSL::TLS1_1_VERSION - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.security_level = 0 + sctx.min_version = 0 + sctx.max_version = OpenSSL::SSL::TLS1_1_VERSION + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.enable_fallback_scsv ctx.security_level = 0 @@ -1856,8 +1920,7 @@ def test_fallback_scsv # Here is not OK # TLS1.2 is supported, fallback to TLS1.1 (downgrade attack) and signaling the fallback # Server support better, so refuse the connection - sock1, sock2 = socketpair - begin + server_proc = proc do |sock| # This test is for the downgrade protection mechanism of TLS1.2. # This is why ctx1 bounds max_version == TLS1.2. # Otherwise, this test fails when using openssl 1.1.1 (or later) that supports TLS1.3. @@ -1866,27 +1929,21 @@ def test_fallback_scsv ctx1.security_level = 0 ctx1.min_version = 0 ctx1.max_version = OpenSSL::SSL::TLS1_2_VERSION - s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - + s1 = OpenSSL::SSL::SSLSocket.new(sock, ctx1) + # AWS-LC has slightly different error messages in all-caps. + assert_raise_with_message(OpenSSL::SSL::SSLError, /inappropriate.fallback/i) { + s1.accept + } + end + start_server_proc(server_proc) do |port| ctx2 = OpenSSL::SSL::SSLContext.new ctx2.enable_fallback_scsv ctx2.security_level = 0 ctx2.min_version = 0 ctx2.max_version = OpenSSL::SSL::TLS1_1_VERSION - s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) - # AWS-LC has slightly different error messages in all-caps. - t = Thread.new { - assert_raise_with_message(OpenSSL::SSL::SSLError, /inappropriate fallback|INAPPROPRIATE_FALLBACK/) { - s2.connect - } + assert_raise_with_message(OpenSSL::SSL::SSLError, /inappropriate.fallback/i) { + server_connect(port, ctx2) { } } - assert_raise_with_message(OpenSSL::SSL::SSLError, /inappropriate fallback|INAPPROPRIATE_FALLBACK/) { - s1.accept - } - t.join - ensure - sock1.close - sock2.close end end @@ -1897,15 +1954,16 @@ def test_tmp_dh_callback dh = Fixtures.pkey("dh-1") called = false - ctx_proc = -> ctx { - ctx.max_version = :TLS1_2 - ctx.ciphers = "DH:!NULL" - ctx.tmp_dh_callback = ->(*args) { - called = true - dh - } + + sctx = make_server_context + sctx.max_version = :TLS1_2 + sctx.ciphers = "DH:!NULL" + sctx.tmp_dh_callback = ->(*args) { + called = true + dh } - start_server(ctx_proc: ctx_proc) do |port| + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.groups = "P-256" # Exclude RFC 7919 groups server_connect(port, ctx) { |ssl| @@ -2005,13 +2063,11 @@ def test_sigalgs ecdsa_key = Fixtures.pkey("p256") ecdsa_cert = issue_cert(@svr, ecdsa_key, 10, svr_exts, @ca_cert, @ca_key) - ctx_proc = -> ctx { - # Unset values set by start_server - ctx.cert = ctx.key = ctx.extra_chain_cert = nil - ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA - ctx.add_certificate(ecdsa_cert, ecdsa_key, [@ca_cert]) # ECDSA - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA + sctx.add_certificate(ecdsa_cert, ecdsa_key, [@ca_cert]) # ECDSA + + start_server(sctx) do |port| ctx1 = OpenSSL::SSL::SSLContext.new ctx1.sigalgs = "rsa_pss_rsae_sha256" server_connect(port, ctx1) { |ssl| @@ -2048,18 +2104,19 @@ def test_client_sigalgs ecdsa_key = Fixtures.pkey("p256") ecdsa_cert = issue_cert(@cli, ecdsa_key, 10, cli_exts, @ca_cert, @ca_key) - ctx_proc = -> ctx { - store = OpenSSL::X509::Store.new - store.add_cert(@ca_cert) - store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - ctx.cert_store = store - ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT - ctx.client_sigalgs = "ECDSA+SHA256" - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + store = OpenSSL::X509::Store.new + store.add_cert(@ca_cert) + store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT + sctx = make_server_context + sctx.cert_store = store + sctx.verify_mode = + OpenSSL::SSL::VERIFY_PEER|OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + sctx.client_sigalgs = "ECDSA+SHA256" + + start_server(sctx, ignore_listener_error: true) do |port| ctx1 = OpenSSL::SSL::SSLContext.new ctx1.add_certificate(@cli_cert, @cli_key) # RSA - assert_handshake_error { + assert_raise(OpenSSL::SSL::SSLError) { server_connect(port, ctx1) { |ssl| ssl.puts("abc"); ssl.gets } @@ -2078,13 +2135,14 @@ def test_get_sigalg # SSL_get0_peer_signature_name() not supported return unless openssl?(3, 5, 0) - server_proc = -> (ctx, ssl) { + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, make_server_context).accept assert_equal('rsa_pss_rsae_sha256', ssl.sigalg) assert_nil(ssl.peer_sigalg) - readwrite_loop(ctx, ssl) - } - start_server(server_proc: server_proc) do |port| + readwrite_loop(ssl) + end + start_server_proc(server_proc) do |port| cli_ctx = OpenSSL::SSL::SSLContext.new server_connect(port, cli_ctx) do |ssl| assert_nil(ssl.sigalg) @@ -2106,20 +2164,18 @@ def test_pqc_sigalg digest: nil) rsa = Fixtures.pkey("rsa-1") rsa_cert = issue_cert(@svr, rsa, 61, [], @ca_cert, @ca_key) - ctx_proc = -> ctx { - # Unset values set by start_server - ctx.cert = ctx.key = ctx.extra_chain_cert = nil - ctx.sigalgs = "rsa_pss_rsae_sha256:mldsa65" - ctx.add_certificate(mldsa_cert, mldsa) - ctx.add_certificate(rsa_cert, rsa) - } - server_proc = -> (ctx, ssl) { - assert_equal('mldsa65', ssl.sigalg) + sctx = OpenSSL::SSL::SSLContext.new + sctx.sigalgs = "rsa_pss_rsae_sha256:mldsa65" + sctx.add_certificate(mldsa_cert, mldsa) + sctx.add_certificate(rsa_cert, rsa) - readwrite_loop(ctx, ssl) - } - start_server(ctx_proc: ctx_proc, server_proc: server_proc) do |port| + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept + assert_equal('mldsa65', ssl.sigalg) + readwrite_loop(ssl) + end + start_server_proc(server_proc) do |port| ctx = OpenSSL::SSL::SSLContext.new # Set signature algorithm because while OpenSSL may use ML-DSA by # default, the system OpenSSL configuration affects the used signature @@ -2131,12 +2187,12 @@ def test_pqc_sigalg } end - server_proc = -> (ctx, ssl) { + server_proc = proc do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept assert_equal('rsa_pss_rsae_sha256', ssl.sigalg) - - readwrite_loop(ctx, ssl) - } - start_server(ctx_proc: ctx_proc, server_proc: server_proc) do |port| + readwrite_loop(ssl) + end + start_server_proc(server_proc) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.sigalgs = 'rsa_pss_rsae_sha256' server_connect(port, ctx) { |ssl| @@ -2149,12 +2205,12 @@ def test_pqc_sigalg def test_connect_works_when_setting_dh_callback_to_nil omit "AWS-LC does not support DHE ciphersuites" if aws_lc? - ctx_proc = -> ctx { - ctx.max_version = :TLS1_2 - ctx.ciphers = "DH:!NULL" # use DH - ctx.tmp_dh_callback = nil - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.max_version = :TLS1_2 + sctx.ciphers = "DH:!NULL" # use DH + sctx.tmp_dh_callback = nil + + start_server(sctx) do |port| assert_nothing_raised { server_connect(port) { } } end end @@ -2165,12 +2221,12 @@ def test_tmp_dh omit "AWS-LC does not support DHE ciphersuites" if aws_lc? dh = Fixtures.pkey("dh-1") - ctx_proc = -> ctx { - ctx.max_version = :TLS1_2 - ctx.ciphers = "DH:!NULL" # use DH - ctx.tmp_dh = dh - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.max_version = :TLS1_2 + sctx.ciphers = "DH:!NULL" # use DH + sctx.tmp_dh = dh + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.groups = "P-256" # Exclude RFC 7919 groups server_connect(port, ctx) { |ssl| @@ -2180,13 +2236,13 @@ def test_tmp_dh end def test_set_groups_tls12 - ctx_proc = -> ctx { - # Enable both ECDHE (~ TLS 1.2) cipher suites and TLS 1.3 - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.ciphers = "kEECDH" - ctx.groups = "P-384:P-521" - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + # Enable both ECDHE (~ TLS 1.2) cipher suites and TLS 1.3 + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.ciphers = "kEECDH" + sctx.groups = "P-384:P-521" + + start_server(sctx, ignore_listener_error: true) do |port| # Test 1: Client=P-256:P-384, Server=P-384:P-521 --> P-384 ctx = OpenSSL::SSL::SSLContext.new ctx.groups = "P-256:P-384" @@ -2224,11 +2280,11 @@ def test_set_groups_tls12 end def test_set_groups_tls13 - ctx_proc = -> ctx { - # Assume TLS 1.3 is enabled and chosen by default - ctx.groups = "P-384:P-521" - } - start_server(ctx_proc: ctx_proc, ignore_listener_error: true) do |port| + # Assume TLS 1.3 is enabled and chosen by default + sctx = make_server_context + sctx.groups = "P-384:P-521" + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.groups = "P-256:P-384" # disable P-521 @@ -2251,10 +2307,10 @@ def test_pqc_group 'SecP256r1MLKEM768', 'SecP384r1MLKEM1024' ].each do |group| - ctx_proc = -> ctx { - ctx.groups = group - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.groups = group + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.groups = group server_connect(port, ctx) { |ssl| @@ -2303,15 +2359,12 @@ def test_security_level def test_dup ctx = OpenSSL::SSL::SSLContext.new - sock1, sock2 = socketpair - ssl = OpenSSL::SSL::SSLSocket.new(sock1, ctx) + Socket.open(:INET, :STREAM) { |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) - assert_raise(NoMethodError) { ctx.dup } - assert_raise(NoMethodError) { ssl.dup } - ensure - ssl.close if ssl - sock1.close - sock2.close + assert_raise(NoMethodError) { ctx.dup } + assert_raise(NoMethodError) { ssl.dup } + } end def test_freeze_calls_setup @@ -2327,17 +2380,14 @@ def test_freeze_calls_setup end def test_fileno - ctx = OpenSSL::SSL::SSLContext.new - sock1, sock2 = socketpair - - socket = OpenSSL::SSL::SSLSocket.new(sock1) - server = OpenSSL::SSL::SSLServer.new(sock2, ctx) + Socket.open(:INET, :STREAM) { |sock| + ctx = OpenSSL::SSL::SSLContext.new + ssl = OpenSSL::SSL::SSLSocket.new(sock) + server = OpenSSL::SSL::SSLServer.new(sock, ctx) - assert_equal socket.fileno, socket.to_io.fileno - assert_equal server.fileno, server.to_io.fileno - ensure - sock1.close - sock2.close + assert_equal(sock.fileno, ssl.fileno) + assert_equal(sock.fileno, server.fileno) + } end def test_export_keying_material @@ -2412,14 +2462,6 @@ def server_connect(port, ctx = nil) sock.close end end - - def assert_handshake_error - # different OpenSSL versions react differently when facing a SSL/TLS version - # that has been marked as forbidden, therefore any of these may be raised - assert_raise(OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::EPIPE) { - yield - } - end end end diff --git a/test/openssl/test_ssl_session.rb b/test/openssl/test_ssl_session.rb index 37874ca27..ab6379385 100644 --- a/test/openssl/test_ssl_session.rb +++ b/test/openssl/test_ssl_session.rb @@ -5,10 +5,10 @@ class OpenSSL::TestSSLSession < OpenSSL::SSLTestCase def test_session - ctx_proc = proc { |ctx| - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + + start_server(sctx) do |port| server_connect_with_session(port, nil, nil) { |ssl| session = ssl.session assert(session == OpenSSL::SSL::Session.new(session.to_pem)) @@ -120,14 +120,15 @@ def test_resumption } } - ctx_proc = proc { |ctx| - ctx.options &= ~OpenSSL::SSL::OP_NO_TICKET - # Disable server-side session cache which is enabled by default - ctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_OFF - # Session tickets must be retrieved via ctx.session_new_cb in TLS 1.3 in AWS-LC. - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION if libressl? || aws_lc? - } - start_server(ctx_proc: ctx_proc) do |port| + sctx = make_server_context + sctx.options &= ~OpenSSL::SSL::OP_NO_TICKET + # Disable server-side session cache which is enabled by default + sctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_OFF + # Session tickets must be retrieved via ctx.session_new_cb in TLS 1.3 in + # AWS-LC. + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION if libressl? || aws_lc? + + start_server(sctx) do |port| sess1 = server_connect_with_session(port, nil, nil) { |ssl| ssl.puts("abc"); assert_equal "abc\n", ssl.gets assert_equal false, ssl.session_reused? @@ -147,14 +148,16 @@ def test_resumption end def test_server_session_cache - ctx_proc = Proc.new do |ctx| - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.options |= OpenSSL::SSL::OP_NO_TICKET - end + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.options |= OpenSSL::SSL::OP_NO_TICKET connections = nil saved_session = nil - server_proc = Proc.new do |ctx, ssl| + + server_proc = Proc.new do |sock| + ssl = OpenSSL::SSL::SSLSocket.new(sock, sctx).accept + ctx = ssl.context stats = ctx.session_cache_stats case connections @@ -194,10 +197,9 @@ def test_server_session_cache assert_equal true, ctx.session_add(saved_session.dup) end - readwrite_loop(ctx, ssl) + readwrite_loop(ssl) end - - start_server(ctx_proc: ctx_proc, server_proc: server_proc) do |port| + start_server_proc(server_proc) do |port| first_session = nil 10.times do |i| connections = i @@ -285,11 +287,12 @@ def test_ctx_client_session_cb_tls13 def test_ctx_client_session_cb_tls13_exception omit "LibreSSL does not call session_new_cb in TLS 1.3" if libressl? - server_proc = lambda do |ctx, ssl| - readwrite_loop(ctx, ssl) - rescue SystemCallError, OpenSSL::SSL::SSLError + sctx = make_server_context + if defined?(OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF) + sctx.options |= OpenSSL::SSL::OP_IGNORE_UNEXPECTED_EOF end - start_server(server_proc: server_proc) do |port| + + start_server(sctx) do |port| ctx = OpenSSL::SSL::SSLContext.new ctx.min_version = :TLS1_3 ctx.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT @@ -308,42 +311,42 @@ def test_ctx_client_session_cb_tls13_exception def test_ctx_server_session_cb connections = nil called = {} + cctx = OpenSSL::SSL::SSLContext.new cctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - sctx = nil - ctx_proc = Proc.new { |ctx| - sctx = ctx - ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION - ctx.options |= OpenSSL::SSL::OP_NO_TICKET - - # get_cb is called whenever a client proposed to resume a session but - # the session could not be found in the internal session cache. - last_server_session = nil - ctx.session_get_cb = lambda { |ary| - _sess, data = ary - called[:get] = data - - if connections == 2 - last_server_session.dup - else - nil - end - } - ctx.session_new_cb = lambda { |ary| - _sock, sess = ary - called[:new] = sess - last_server_session = sess - } - - if TEST_SESSION_REMOVE_CB - ctx.session_remove_cb = lambda { |ary| - _ctx, sess = ary - called[:remove] = sess - } + sctx = make_server_context + sctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + sctx.options |= OpenSSL::SSL::OP_NO_TICKET + + # get_cb is called whenever a client proposed to resume a session but + # the session could not be found in the internal session cache. + last_server_session = nil + sctx.session_get_cb = lambda { |ary| + _sess, data = ary + called[:get] = data + + if connections == 2 + last_server_session.dup + else + nil end } - start_server(ctx_proc: ctx_proc) do |port| + + sctx.session_new_cb = lambda { |ary| + _sock, sess = ary + called[:new] = sess + last_server_session = sess + } + + if TEST_SESSION_REMOVE_CB + sctx.session_remove_cb = lambda { |ary| + _ctx, sess = ary + called[:remove] = sess + } + end + + start_server(sctx) do |port| connections = 0 sess0 = server_connect_with_session(port, cctx, nil) { |ssl| ssl.puts("abc"); assert_equal "abc\n", ssl.gets diff --git a/test/openssl/ut_eof.rb b/test/openssl/ut_eof.rb index 06aa632a6..807bbd015 100644 --- a/test/openssl/ut_eof.rb +++ b/test/openssl/ut_eof.rb @@ -4,6 +4,17 @@ if defined?(OpenSSL) module OpenSSL::TestEOF + def open_file(content) + ssl_pair { |s1, s2| + begin + th = Thread.new { s2 << content; s2.close } + yield s1 + ensure + th&.join + end + } + end + def test_getbyte_eof open_file("") {|f| assert_nil f.getbyte } end diff --git a/test/openssl/utils.rb b/test/openssl/utils.rb index 7e6fe8b16..7dd3a719d 100644 --- a/test/openssl/utils.rb +++ b/test/openssl/utils.rb @@ -190,93 +190,87 @@ def setup @server = nil end - def readwrite_loop(ctx, ssl) + def readwrite_loop(ssl) while line = ssl.gets ssl.write(line) end + ssl.close end - def start_server(verify_mode: OpenSSL::SSL::VERIFY_NONE, - ctx_proc: nil, server_proc: method(:readwrite_loop), - accept_proc: proc{}, - ignore_listener_error: false, &block) - IO.pipe {|stop_pipe_r, stop_pipe_w| - ctx = OpenSSL::SSL::SSLContext.new - ctx.cert = @svr_cert - ctx.key = @svr_key - ctx.verify_mode = verify_mode - ctx_proc.call(ctx) if ctx_proc - - Socket.do_not_reverse_lookup = true + def make_server_context + sctx = OpenSSL::SSL::SSLContext.new + sctx.add_certificate(@svr_cert, @svr_key) + sctx + end + + def start_server_proc(server_proc, &block) + IO.pipe do |stop_pipe_r, stop_pipe_w| tcps = TCPServer.new("127.0.0.1", 0) + tcps.setsockopt(:TCP, :NODELAY, 1) port = tcps.connect_address.ip_port - ssls = OpenSSL::SSL::SSLServer.new(tcps, ctx) - threads = [] - begin - server_thread = Thread.new do - Thread.current.report_on_exception = false - - begin - loop do - begin - readable, = IO.select([ssls, stop_pipe_r]) - break if readable.include? stop_pipe_r - ssl = ssls.accept - accept_proc.call(ssl) - rescue OpenSSL::SSL::SSLError, IOError, Errno::EBADF, Errno::EINVAL, - Errno::ECONNABORTED, Errno::ENOTSOCK, Errno::ECONNRESET - retry if ignore_listener_error - raise - end - - th = Thread.new do - Thread.current.report_on_exception = false - - begin - server_proc.call(ctx, ssl) - ensure - ssl.close - end - true - end - threads << th - end - ensure - tcps.close - end - end + sockets = [] + server_thread = Thread.new do + Thread.current.report_on_exception = false + + loop do + readable, = IO.select([tcps, stop_pipe_r]) + break if readable.include? stop_pipe_r + sockets << sock = tcps.accept - client_thread = Thread.new do - Thread.current.report_on_exception = false + th = Thread.new do + Thread.current.report_on_exception = false - begin - block.call(port) - ensure - # Stop accepting new connection - stop_pipe_w.close - server_thread.join + server_proc.call(sock) end + threads << th end - threads.unshift client_thread ensure - # Terminate existing connections. If a thread did 'pend', re-raise it. - pend = nil - threads.each { |th| - begin - timeout = EnvUtil.apply_timeout_scale(30) - th.join(timeout) or - th.raise(RuntimeError, "[start_server] thread did not exit in #{timeout} secs") - rescue Test::Unit::PendedError - pend = $! - rescue Exception - end - } - raise pend if pend - assert_join_threads(threads) + tcps.close + end + + client_thread = Thread.new do + Thread.current.report_on_exception = false + + block.call(port) + ensure + # Stop accepting new connection + stop_pipe_w.close + server_thread.join + end + threads.unshift client_thread + ensure + # Terminate existing connections. If a thread did 'pend', re-raise it. + pend = nil + threads.each { |th| + begin + timeout = EnvUtil.apply_timeout_scale(30) + th.join(timeout) or + th.raise(RuntimeError, "[start_server] thread did not exit in #{timeout} secs") + rescue Test::Unit::PendedError + pend = $! + rescue Exception + end + } + sockets.each(&:close) + raise pend if pend + assert_join_threads(threads) + end + end + + def start_server(ctx = make_server_context, ignore_listener_error: false, &block) + server_proc = -> sock { + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) + begin + ssl.accept + rescue OpenSSL::SSL::SSLError, Errno::ECONNABORTED, Errno::ECONNRESET + next if ignore_listener_error + raise end + readwrite_loop(ssl) } + start_server_proc(server_proc, &block) end end