diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 2ab89fb57..825c2a17e 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -38,7 +38,7 @@ static VALUE eSSLErrorWaitWritable; static ID id_call, ID_callback_state, id_npn_protocols_encoded, id_each, id_original_context; -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, @@ -1661,8 +1661,9 @@ fallback_peer_ip_address(VALUE self, VALUE exc) } static VALUE -peeraddr_ip_str(VALUE io) +peeraddr_ip_str(VALUE self) { + VALUE io = rb_attr_get(self, id_i_io); return rb_rescue2(peer_ip_address, io, fallback_peer_ip_address, Qnil, rb_eSystemCallError, (VALUE)0); } @@ -1778,497 +1779,248 @@ 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(); +#ifdef __APPLE__ + retry:; +#endif + 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)); - } + 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) - 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: - if (saved_errno) { - ossl_clear_error(); + int code = SSL_get_error(ssl, ret); + switch (code) { + case SSL_ERROR_NONE: + return self; + 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(); #ifdef __APPLE__ - /* See ossl_ssl_write_internal() */ - if (saved_errno == EPROTOTYPE) - continue; + /* See ossl_ssl_write_internal() */ + if (saved_errno == EPROTOTYPE) + goto retry; #endif - rb_exc_raise(rb_syserr_new(saved_errno, funcname)); - } - /* fallthrough */ - default: { - VALUE error_append = Qnil; + rb_exc_raise(rb_syserr_new(saved_errno, funcname)); + } + /* fallthrough */ + default: { + VALUE str = rb_sprintf( + "%s%s returned=%d errno=%d peeraddr=%"PRIsVALUE" state=%s", + funcname, + code == SSL_ERROR_SYSCALL ? " SYSCALL" : "", + code, + saved_errno, + peeraddr_ip_str(self), + SSL_state_string_long(ssl) + ); #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) { + long verify_result = SSL_get_verify_result(ssl); + const char *err_msg = ERR_reason_error_string(err), + *verify_msg = X509_verify_cert_error_string(verify_result); + if (!err_msg) + err_msg = "(null)"; + if (!verify_msg) + verify_msg = "(null)"; + ossl_clear_error(); /* let ossl_raise() not append message */ + rb_str_catf(str, ": %s (%s)", err_msg, verify_msg); } +#endif + rb_exc_raise(ossl_make_error(eSSLError, str)); + } } - - return self; } /* * call-seq: - * ssl.connect => self - * - * Initiates an SSL/TLS handshake with a server. + * ssl.ssl_connect -> self, :wait_readable, or :wait_writable */ static VALUE ossl_ssl_connect(VALUE self) { ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_connect, "SSL_connect", Qfalse); + return ossl_start_ssl(self, SSL_connect, "SSL_connect"); } /* * 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. + * ssl.ssl_accept -> self, :wait_readable, or :wait_writable */ static VALUE -ossl_ssl_connect_nonblock(int argc, VALUE *argv, VALUE self) +ossl_ssl_accept(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); + return ossl_start_ssl(self, SSL_accept, "SSL_accept"); } /* - * call-seq: - * ssl.accept => self - * - * Waits for a SSL/TLS client to initiate a handshake. + * TODO: This shouldn't be necessary, but we do not want to use + * String.new(capacity: len) in #sysread because it allocates Hash objects + * for the kwargs. */ static VALUE -ossl_ssl_accept(VALUE self) +ossl_ssl_string_new_capacity(VALUE self, VALUE len) { - ossl_ssl_setup(self); - - return ossl_start_ssl(self, SSL_accept, "SSL_accept", Qfalse); + rb_len_t capa = NUM2LEN(len); + return rb_str_buf_new(capa >= 0 ? capa : 0); } /* * 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. + * ssl.ssl_read(len, str) -> str, nil, :wait_readable, or :wait_writable */ static VALUE -ossl_ssl_accept_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_accept, "SSL_accept", opts); -} - -static VALUE -ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) +ossl_ssl_read(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"); ilen = NUM2INT(len); - if (NIL_P(str)) - str = rb_str_new(0, ilen); - else { - StringValue(str); - if (RSTRING_LEN(str) >= ilen) - rb_str_modify(str); - else - rb_str_modify_expand(str, ilen - RSTRING_LEN(str)); - } + StringValue(str); + if ((rb_len_t)rb_str_capacity(str) >= ilen) + rb_str_modify(str); + else + rb_str_modify_expand(str, ilen - RSTRING_LEN(str)); if (ilen == 0) { rb_str_set_len(str, 0); return str; } - VALUE io = rb_attr_get(self, id_i_io); + rb_str_locktmp(str); + int nread = SSL_read(ssl, RSTRING_PTR(str), ilen); + rb_str_unlocktmp(str); + int saved_errno = errno_mapped(); - 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); + 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 (saved_errno) { ossl_clear_error(); - rb_jump_tag(NUM2INT(cb_state)); + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); } - - 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 (saved_errno) { - ossl_clear_error(); - rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); - } - if (!ERR_peek_error()) { - /* - * 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(); - } - /* fall through */ - default: - ossl_raise(eSSLError, "SSL_read"); + if (!ERR_peek_error()) { + /* + * 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; } - - // 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. + * ssl.ssl_write(str) -> integer, :wait_readable, or :wait_writable */ 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(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; VALUE cb_state; GetSSL(self, ssl); if (!ssl_started(ssl)) rb_raise(eSSLError, "SSL session is not started yet"); - VALUE io = rb_attr_get(self, id_i_io); - GetOpenFile(io, fptr); - + StringValue(str); /* SSL_write(3ssl) manpage states num == 0 is undefined */ - num = RSTRING_LENINT(str); + int 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: - if (saved_errno) { - ossl_clear_error(); #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; + retry:; #endif - 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); + int 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: + if (saved_errno) { + ossl_clear_error(); +#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 + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_write")); + } + /* fallthrough */ + default: + ossl_raise(eSSLError, "SSL_write"); + } } /* @@ -3199,14 +2951,11 @@ 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, "ssl_connect", ossl_ssl_connect, 0); + rb_define_private_method(cSSLSocket, "ssl_accept", ossl_ssl_accept, 0); + 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, "cert", ossl_ssl_get_cert, 0); rb_define_method(cSSLSocket, "peer_cert", ossl_ssl_get_peer_cert, 0); @@ -3384,8 +3133,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 9edbcb62e..85a91dee2 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -14,6 +14,7 @@ if defined?(OpenSSL::SSL) +require "io/wait" if RUBY_VERSION < "3.2" require "io/nonblock" require "ipaddr" require "socket" @@ -252,6 +253,15 @@ def timeout def timeout=(value) to_io.timeout=(value) end + else + def timeout + nil + end + + def timeout=(value) + raise NotImplementedError, + "IO#timeout= is not available on this Ruby version" + end end end @@ -369,6 +379,169 @@ 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 blocking(timeout = self.timeout) + if timeout + remaining = timeout + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + while true + ret = yield + if :wait_readable == ret + io.wait_readable(remaining) or raise IO_TimeoutError, + "Timed out while waiting to become readable!" + elsif :wait_writable == ret + io.wait_writable(remaining) or raise IO_TimeoutError, + "Timed out while waiting to become writable!" + else + return ret + end + + if timeout + now = Process.clock_gettime(Process::CLOCK_MONOTONIC) + remaining = [0, timeout - (now - start)].max + end + end + end + + private def check_nonblock(ret) + if :wait_readable == ret + raise SSLErrorWaitReadable, "read would block" + elsif :wait_writable == ret + raise SSLErrorWaitWritable, "write would block" + else + ret + end + end + + # :call-seq: + # ssl.connect(timeout: ssl.timeout) -> 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: self.timeout) + blocking(timeout) { ssl_connect } + end + + # :call-seq: + # ssl.connect_nonblock -> self + # ssl.connect_nonblock(exception: false) -> self, :wait_readable, or :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 = ssl_connect + check_nonblock(ret) if exception + ret + end + + # :call-seq: + # ssl.accept(timeout: ssl.timeout) -> 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: self.timeout) + blocking(timeout) { ssl_accept } + end + + # :call-seq: + # ssl.accept_nonblock -> self + # ssl.accept_nonblock(exception: false) -> self, :wait_readable, or :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 = ssl_accept + 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) + buffer ||= string_new_capacity(length) + blocking { + ssl_read(length, buffer) or raise EOFError, "end of file reached" + } + end + + # :call-seq: + # ssl.sysread_nonblock(length) -> string + # ssl.sysread_nonblock(length, buffer) -> buffer + # ssl.sysread_nonblock(length, buffer, exception: false) -> buffer, nil, :wait_readable, or :wait_writable + # + # A non-blocking version of #sysread. + private def sysread_nonblock(length, buffer = nil, exception: true) + buffer ||= string_new_capacity(length) + ret = ssl_read(length, buffer) + if exception + raise EOFError, "end of file reached" if ret.nil? + check_nonblock(ret) + end + ret + end + + # :call-seq: + # ssl.syswrite(string) -> Integer + # + # Writes _string_ to the SSL connection. + def syswrite(string) + blocking { ssl_write(string) } + end + + # :call-seq: + # ssl.syswrite_nonblock(string) -> Integer + # ssl.syswrite_nonblock(string, exception: false) -> Integer, :wait_readable, or :wait_writable + # + # Writes _string_ to the SSL connection in a non-blocking manner. + private def syswrite_nonblock(string, exception: true) + ret = ssl_write(string) + check_nonblock(ret) if exception + ret + end + # call-seq: # ssl.post_connection_check(hostname) -> true # diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 90f464141..f86d60ed8 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -186,6 +186,49 @@ def test_connect_accept_nonblock 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 round trips + ctx = make_server_context + ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) + + sleep 0.05 + assert_equal(:wait_readable, ssl.accept_nonblock(exception: false)) + sleep 0.15 + assert_same(ssl, ssl.accept_nonblock(exception: false)) + + 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.1) } + 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)