Skip to content

Commit 7999e1e

Browse files
authored
Merge pull request #976 from rhenium/ky/err-extras
ossl.c: implement OpenSSL::OpenSSLError#detailed_message
2 parents a1ce67c + d28f7a9 commit 7999e1e

File tree

3 files changed

+100
-16
lines changed

3 files changed

+100
-16
lines changed

ext/openssl/extconf.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,12 @@
3838

3939
$defs.push("-D""OPENSSL_SUPPRESS_DEPRECATED")
4040

41+
# Missing in TruffleRuby
42+
have_func("rb_call_super_kw(0, NULL, 0)", "ruby.h")
43+
# Ruby 3.1
4144
have_func("rb_io_descriptor", "ruby/io.h")
42-
have_func("rb_io_maybe_wait(0, Qnil, Qnil, Qnil)", "ruby/io.h") # Ruby 3.1
45+
have_func("rb_io_maybe_wait(0, Qnil, Qnil, Qnil)", "ruby/io.h")
46+
# Ruby 3.2
4347
have_func("rb_io_timeout", "ruby/io.h")
4448

4549
Logging::message "=== Checking for system dependent stuff... ===\n"

ext/openssl/ossl.c

Lines changed: 81 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,17 @@ ossl_to_der_if_possible(VALUE obj)
254254
/*
255255
* Errors
256256
*/
257+
static ID id_i_errors;
258+
259+
static void collect_errors_into(VALUE ary);
260+
257261
VALUE
258262
ossl_make_error(VALUE exc, VALUE str)
259263
{
260264
unsigned long e;
261265
const char *data;
262266
int flags;
267+
VALUE errors = rb_ary_new();
263268

264269
if (NIL_P(str))
265270
str = rb_str_new(NULL, 0);
@@ -276,10 +281,12 @@ ossl_make_error(VALUE exc, VALUE str)
276281
rb_str_cat_cstr(str, msg ? msg : "(null)");
277282
if (flags & ERR_TXT_STRING && data)
278283
rb_str_catf(str, " (%s)", data);
279-
ossl_clear_error();
284+
collect_errors_into(errors);
280285
}
281286

282-
return rb_exc_new_str(exc, str);
287+
VALUE obj = rb_exc_new_str(exc, str);
288+
rb_ivar_set(obj, id_i_errors, errors);
289+
return obj;
283290
}
284291

285292
void
@@ -300,13 +307,12 @@ ossl_raise(VALUE exc, const char *fmt, ...)
300307
rb_exc_raise(ossl_make_error(exc, err));
301308
}
302309

303-
void
304-
ossl_clear_error(void)
310+
static void
311+
collect_errors_into(VALUE ary)
305312
{
306-
if (dOSSL == Qtrue) {
313+
if (dOSSL == Qtrue || !NIL_P(ary)) {
307314
unsigned long e;
308315
const char *file, *data, *func, *lib, *reason;
309-
char append[256] = "";
310316
int line, flags;
311317

312318
#ifdef HAVE_ERR_GET_ERROR_ALL
@@ -318,20 +324,66 @@ ossl_clear_error(void)
318324
lib = ERR_lib_error_string(e);
319325
reason = ERR_reason_error_string(e);
320326

327+
VALUE str = rb_sprintf("error:%08lX:%s:%s:%s", e, lib ? lib : "",
328+
func ? func : "", reason ? reason : "");
321329
if (flags & ERR_TXT_STRING) {
322330
if (!data)
323331
data = "(null)";
324-
snprintf(append, sizeof(append), " (%s)", data);
332+
rb_str_catf(str, " (%s)", data);
325333
}
326-
rb_warn("error on stack: error:%08lX:%s:%s:%s%s", e, lib ? lib : "",
327-
func ? func : "", reason ? reason : "", append);
334+
335+
if (dOSSL == Qtrue)
336+
rb_warn("error on stack: %"PRIsVALUE, str);
337+
if (!NIL_P(ary))
338+
rb_ary_push(ary, str);
328339
}
329340
}
330341
else {
331342
ERR_clear_error();
332343
}
333344
}
334345

346+
void
347+
ossl_clear_error(void)
348+
{
349+
collect_errors_into(Qnil);
350+
}
351+
352+
/*
353+
* call-seq:
354+
* ossl_error.detailed_message(**) -> string
355+
*
356+
* Returns the exception message decorated with the captured \OpenSSL error
357+
* queue entries.
358+
*/
359+
static VALUE
360+
osslerror_detailed_message(int argc, VALUE *argv, VALUE self)
361+
{
362+
VALUE str;
363+
#ifdef HAVE_RB_CALL_SUPER_KW
364+
// Ruby >= 3.2
365+
if (RTEST(rb_funcall(rb_eException, rb_intern("method_defined?"), 1,
366+
ID2SYM(rb_intern("detailed_message")))))
367+
str = rb_call_super_kw(argc, argv, RB_PASS_CALLED_KEYWORDS);
368+
else
369+
#endif
370+
str = rb_funcall(self, rb_intern("message"), 0);
371+
VALUE errors = rb_attr_get(self, id_i_errors);
372+
373+
// OpenSSLError was not created by ossl_make_error()
374+
if (!RB_TYPE_P(errors, T_ARRAY))
375+
return str;
376+
377+
str = rb_str_resurrect(str);
378+
rb_str_catf(str, "\nOpenSSL error queue reported %ld errors:",
379+
RARRAY_LEN(errors));
380+
for (long i = 0; i < RARRAY_LEN(errors); i++) {
381+
VALUE err = RARRAY_AREF(errors, i);
382+
rb_str_catf(str, "\n%"PRIsVALUE, err);
383+
}
384+
return str;
385+
}
386+
335387
/*
336388
* call-seq:
337389
* OpenSSL.errors -> [String...]
@@ -1009,10 +1061,26 @@ Init_openssl(void)
10091061

10101062
rb_global_variable(&eOSSLError);
10111063
/*
1012-
* Generic error,
1013-
* common for all classes under OpenSSL module
1064+
* Generic error class for OpenSSL. All error classes in this library
1065+
* inherit from this class.
1066+
*
1067+
* This class indicates that an error was reported by the underlying
1068+
* \OpenSSL library.
1069+
*/
1070+
eOSSLError = rb_define_class_under(mOSSL, "OpenSSLError", rb_eStandardError);
1071+
/*
1072+
* \OpenSSL error queue entries captured at the time the exception was
1073+
* raised. The same information is printed to stderr if OpenSSL.debug is
1074+
* set to +true+.
1075+
*
1076+
* This is an array of zero or more strings, ordered from the oldest to the
1077+
* newest. The format of the strings is not stable and may vary across
1078+
* versions of \OpenSSL or versions of this Ruby extension.
1079+
*
1080+
* See also the man page ERR_get_error(3).
10141081
*/
1015-
eOSSLError = rb_define_class_under(mOSSL,"OpenSSLError",rb_eStandardError);
1082+
rb_attr(eOSSLError, rb_intern_const("errors"), 1, 0, 0);
1083+
rb_define_method(eOSSLError, "detailed_message", osslerror_detailed_message, -1);
10161084

10171085
/*
10181086
* Init debug core
@@ -1028,6 +1096,7 @@ Init_openssl(void)
10281096
* Get ID of to_der
10291097
*/
10301098
ossl_s_to_der = rb_intern("to_der");
1099+
id_i_errors = rb_intern("@errors");
10311100

10321101
/*
10331102
* Init components

test/openssl/test_ossl.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,27 @@ def test_memcmp_timing
6666
end if ENV["OSSL_TEST_ALL"] == "1"
6767

6868
def test_error_data
69-
# X509V3_EXT_nconf_nid() called from OpenSSL::X509::ExtensionFactory#create_ext is a function
70-
# that uses ERR_raise_data() to append additional information about the error.
69+
# X509V3_EXT_nconf_nid() called from
70+
# OpenSSL::X509::ExtensionFactory#create_ext is a function that uses
71+
# ERR_raise_data() to append additional information about the error.
7172
#
7273
# The generated message should look like:
7374
# "subjectAltName = IP:not.a.valid.ip.address: bad ip address (value=not.a.valid.ip.address)"
7475
# "subjectAltName = IP:not.a.valid.ip.address: error in extension (name=subjectAltName, value=IP:not.a.valid.ip.address)"
76+
#
77+
# The string inside parentheses is the ERR_TXT_STRING data, and is appended
78+
# by ossl_make_error(), so we check it here.
7579
ef = OpenSSL::X509::ExtensionFactory.new
76-
assert_raise_with_message(OpenSSL::X509::ExtensionError, /value=(IP:)?not.a.valid.ip.address\)/) {
80+
e = assert_raise(OpenSSL::X509::ExtensionError) {
7781
ef.create_ext("subjectAltName", "IP:not.a.valid.ip.address")
7882
}
83+
assert_match(/not.a.valid.ip.address\)\z/, e.message)
84+
85+
# We currently craft the strings based on ERR_error_string()'s style:
86+
# error:<error code in hex>:<library>:<function>:<reason> (data)
87+
assert_instance_of(Array, e.errors)
88+
assert_match(/\Aerror:.*not.a.valid.ip.address\)\z/, e.errors.last)
89+
assert_include(e.detailed_message, "not.a.valid.ip.address")
7990
end
8091
end
8192

0 commit comments

Comments
 (0)