Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promote some OpenSSL warnings to Errors #5111

Closed
wants to merge 4 commits into from

Conversation

Girgias
Copy link
Member

@Girgias Girgias commented Jan 24, 2020

So I went pretty conservative here with changes which seems non controversial.

@bukka
Copy link
Member

bukka commented Feb 24, 2020

Was there RFC for this?

@kocsismate
Copy link
Member

@bukka It belongs to the https://wiki.php.net/rfc/engine_warnings RFC.

@bukka
Copy link
Member

bukka commented Feb 26, 2020

I would get the incorrect type (e.g. overflows) being classified like that. However the RFC doesn't cover things like "algorithm not found". There is lots of code that depends on this so this is not a good idea to do and it has nothing to do with the linked RFC.

@Girgias
Copy link
Member Author

Girgias commented Feb 26, 2020

I would get the incorrect type (e.g. overflows) being classified like that. However the RFC doesn't cover things like "algorithm not found". There is lots of code that depends on this so this is not a good idea to do and it has nothing to do with the linked RFC.

So for the most part these conversions comes from Nikita's comment on a PoC I made last summer: #4549 (comment)

This resulted in a lot of warnings being promoted to Error/ValueError/TypeError (the type error one does have the consistent TypeError RFC to back it up).

However, as I don't know how most of the extensions work I mostly chugger through by converting things which seem reasonable to me that's why it's on review to see if some of them would be very disruptive.

I hope that you do agree that these conversions have value for stuff which is plainly false (such as type errors, or wrong flags/values).

To address the "algorithm not found" promotion, this may well be not wise, is it because some algorithms may be available on some versions of OpenSSL and not on others? In that case not promoting it would make a lot of sense.

These promotions are there to flag obvious and guaranteed mistakes, so that clearly broken code indicates that its broken. Not really making it hard to upgrade (for the most part).

@Girgias Girgias force-pushed the openssl-warning-promotion branch 2 times, most recently from 6cd85d8 to 34b6a03 Compare March 27, 2020 01:31
@Girgias Girgias force-pushed the openssl-warning-promotion branch 3 times, most recently from bae4a20 to d503262 Compare April 22, 2020 00:52
@@ -780,7 +777,7 @@ static int php_openssl_parse_config(struct php_x509_request * req, zval * option
zend_long cipher_algo = Z_LVAL_P(item);
const EVP_CIPHER* cipher = php_openssl_get_evp_cipher_from_algo(cipher_algo);
if (cipher == NULL) {
php_error_docref(NULL, E_WARNING, "Unknown cipher algorithm for private key.");
zend_value_error("Unknown cipher algorithm for private key.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The punctuation mark is to be avoided :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, in case of issues with array items of arguments, we usually show a similar message to this:
"deflate_init(): \"memory\" option must be between 1 and 9".

Maybe this message could be improved by also showing the argument name somehow. We just haven't found a good way yet to do so.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same comment applies to the 2 changes below.


mdtype = php_openssl_get_evp_md_from_algo(algo);
if (!mdtype) {
zend_argument_value_error(3, "Unknown signature algorithm");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
zend_argument_value_error(3, "Unknown signature algorithm");
zend_argument_value_error(3, "must be a valid signature algorithm");

or

Suggested change
zend_argument_value_error(3, "Unknown signature algorithm");
zend_argument_value_error(3, "is an unknown signature algorithm");

I think we use the first format more often


if (x509 == NULL) {
php_error_docref(NULL, E_WARNING, "Supplied parameter cannot be coerced into an X509 certificate!");
RETURN_FALSE;
zend_argument_type_error(1, "cannot be coerced into an X509 certificate");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sounds very weird. Couldn't we use must be of type string or OpenSSL-resource, %s given?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been a while as I was just rebasing, but there are multiple OpenSSL resources (see: https://www.php.net/manual/en/openssl.resources.php) so that message wouldn't be correct either, maybe we should first work on the resource to object conversion and then fave another look at promoting the warnings?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conversion is done :)

@@ -2958,9 +2962,9 @@ PHP_FUNCTION(openssl_csr_export_to_file)
csr = php_openssl_csr_from_zval(zcsr, 0, &csr_resource);
if (csr == NULL) {
if (!EG(exception)) {
php_error_docref(NULL, E_WARNING, "Cannot get CSR from parameter 1");
zend_argument_type_error(1, "cannot be coerced into an Certificate Signing Request (CSR)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A similar message could be used what I suggested in case of the X509 certificate

@@ -3427,7 +3431,7 @@ static EVP_PKEY * php_openssl_evp_from_zval(
/* get passphrase */

if ((zphrase = zend_hash_index_find(Z_ARRVAL_P(val), 1)) == NULL) {
php_error_docref(NULL, E_WARNING, "Key array must be of the form array(0 => key, 1 => phrase)");
zend_value_error("Key array must be of the form array(0 => key, 1 => phrase)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
zend_value_error("Key array must be of the form array(0 => key, 1 => phrase)");
zend_argument_value_error(val_argnum, "must be of the form array(0 => key, 1 => phrase)");

@@ -3588,7 +3592,7 @@ static EVP_PKEY * php_openssl_generate_private_key(struct php_x509_request * req
EVP_PKEY * return_val = NULL;

if (req->priv_key_bits < MIN_KEY_LENGTH) {
php_error_docref(NULL, E_WARNING, "Private key length is too short; it needs to be at least %d bits, not %d",
zend_value_error("Private key length is too short; it needs to be at least %d bits, not %d",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
zend_value_error("Private key length is too short; it needs to be at least %d bits, not %d",
zend_argument_value_error(argnum, "must be at least %d bits", ...);

@@ -4082,7 +4086,7 @@ PHP_FUNCTION(openssl_pkey_new)
}

if (group == NULL) {
php_error_docref(NULL, E_WARNING, "Unknown curve_name");
zend_argument_value_error(1, "Unknown curve_name");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
zend_argument_value_error(1, "Unknown curve_name");
zend_argument_value_error(1, "must be a valid curve name");

if (key == NULL) {
if (!EG(exception)) {
php_error_docref(NULL, E_WARNING, "Cannot get key from parameter 1");
// TypeError?
zend_argument_value_error(1, "cannot retrieve key");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like:

Suggested change
zend_argument_value_error(1, "cannot retrieve key");
zend_argument_value_error(1, "doesn't have a key");

I'm not exactly sure how to phrase this though

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't seem to be correct looking at the implementation of php_openssl_evp_from_zval() which looks it can fail for weird reasons.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I drop this ?

if (key == NULL) {
if (!EG(exception)) {
php_error_docref(NULL, E_WARNING, "Cannot get key from parameter 1");
zend_argument_value_error(1, "cannot retrieve key");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as before

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoud I drop this?

@@ -4718,15 +4733,10 @@ PHP_FUNCTION(openssl_pbkdf2)
}

if (!digest) {
php_error_docref(NULL, E_WARNING, "Unknown signature algorithm");
RETURN_FALSE;
zend_argument_value_error(5, "Unknown signature algorithm");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad format :)

@Girgias
Copy link
Member Author

Girgias commented Jun 22, 2020

@kocsismate I think I've address all the error messages so that they adhere to the common format, however I did not address the content of them when they needed me to start passing the argument number, reason why is that most are part of the extension API and I'm not exactly sure how to go about this.

@Girgias Girgias force-pushed the openssl-warning-promotion branch 2 times, most recently from 08aab82 to 8c1fdd2 Compare June 22, 2020 17:42
/* check if size_t can be safely casted to unsigned int */
#define PHP_OPENSSL_CHECK_SIZE_T_TO_UINT(_var, _name) \
PHP_OPENSSL_CHECK_NUMBER_CONVERSION(ZEND_SIZE_T_UINT_OVFL(_var), _name)
#define PHP_OPENSSL_CHECK_SIZE_T_TO_UINT(_var, _name, arg_num) \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use _arg_num - macro arguments should start with _

#define PHP_OPENSSL_CHECK_SIZE_T_TO_INT_NORET(_var, _name) \
PHP_OPENSSL_CHECK_NUMBER_CONVERSION_NORET(ZEND_SIZE_T_INT_OVFL(_var), _name)
#define PHP_OPENSSL_CHECK_SIZE_T_TO_INT(_var, _name, arg_num) \
PHP_OPENSSL_CHECK_NUMBER_CONVERSION(ZEND_SIZE_T_INT_OVFL(_var), _name, arg_num)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use _arg_num - macro arguments should start with _

/* check if long can be safely casted to int */
#define PHP_OPENSSL_CHECK_LONG_TO_INT_NORET(_var, _name) \
PHP_OPENSSL_CHECK_NUMBER_CONVERSION_NORET(ZEND_LONG_EXCEEDS_INT(_var), _name)
#define PHP_OPENSSL_CHECK_LONG_TO_INT(_var, _name, arg_num) \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use _arg_num - macro arguments should start with _

@@ -537,14 +534,14 @@ static time_t php_openssl_asn1_time_to_time_t(ASN1_UTCTIME * timestr) /* {{{ */
size_t timestr_len;

if (ASN1_STRING_type(timestr) != V_ASN1_UTCTIME && ASN1_STRING_type(timestr) != V_ASN1_GENERALIZEDTIME) {
php_error_docref(NULL, E_WARNING, "Illegal ASN1 data type for timestamp");
zend_value_error("Illegal ASN1 data type for timestamp");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not change this one as it would be inconsistent with some warnings below. This is used just by openssl_x509_parse so there is still value to not fail the whole operation to see the rest of the fields for in this case incorrectly formatted cert.

return (time_t)-1;
}

timestr_len = (size_t)ASN1_STRING_length(timestr);

if (timestr_len != strlen((const char *)ASN1_STRING_get0_data(timestr))) {
php_error_docref(NULL, E_WARNING, "Illegal length in timestamp");
zend_value_error("Illegal length in timestamp");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same as above - please do not change this one.

@@ -780,7 +777,7 @@ static int php_openssl_parse_config(struct php_x509_request * req, zval * option
zend_long cipher_algo = Z_LVAL_P(item);
const EVP_CIPHER* cipher = php_openssl_get_evp_cipher_from_algo(cipher_algo);
if (cipher == NULL) {
php_error_docref(NULL, E_WARNING, "Unknown cipher algorithm for private key.");
zend_value_error("Unknown cipher algorithm for private key");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this cipher is used only if there is a private key passphrase and it can be caused by invalid conflict but doesn't need to be an issue because passprhase is not always used or it can be just something wrong in the config. I'd keep warning in here rather than prevent the whole operation to minimize BC breaks.

@@ -811,7 +808,7 @@ static int php_openssl_parse_config(struct php_x509_request * req, zval * option
&& Z_TYPE_P(item) == IS_STRING) {
req->curve_name = OBJ_sn2nid(Z_STRVAL_P(item));
if (req->curve_name == NID_undef) {
php_error_docref(NULL, E_WARNING, "Unknown elliptic curve (short) name %s", Z_STRVAL_P(item));
zend_value_error("Unknown elliptic curve (short) name %s", Z_STRVAL_P(item));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again this doesn't need to be always an issue so please keep it as warning.

@@ -822,7 +819,7 @@ static int php_openssl_parse_config(struct php_x509_request * req, zval * option
if (str == NULL) {
php_openssl_store_errors();
} else if (!ASN1_STRING_set_default_mask_asc(str)) {
php_error_docref(NULL, E_WARNING, "Invalid global string mask setting %s", str);
zend_value_error("Invalid global string mask setting %s", str);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above - please keep warning here

@@ -3588,7 +3592,7 @@ static EVP_PKEY * php_openssl_generate_private_key(struct php_x509_request * req
EVP_PKEY * return_val = NULL;

if (req->priv_key_bits < MIN_KEY_LENGTH) {
php_error_docref(NULL, E_WARNING, "Private key length is too short; it needs to be at least %d bits, not %d",
zend_value_error("Private key length is too short; it needs to be at least %d bits, not %d",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you are changing this, you should change this one as well:

php_error_docref(NULL, E_WARNING, "Unsupported private key type");

@@ -4939,6 +4949,7 @@ PHP_FUNCTION(openssl_pkcs7_encrypt)

cert = php_openssl_x509_from_zval(zcertval, 0, &certresource);
if (cert == NULL) {
// TODO Convert to ValueError?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah that should be done for consistency

ext/openssl/openssl.c Outdated Show resolved Hide resolved
@bukka
Copy link
Member

bukka commented Aug 3, 2020

@Girgias @kocsismate @nikic So I have checked out the branch and have been looking closely on this. I think x509 coercion errors make things actually quite inconsistent. For example it throws if you pass an invalid input file (e.g. when path is invalid) but for output file parameters it is just a warning (e.g. the output files). Then there are bunch of other warnings that are result of the parameters that are passed in or potential failures in openssl and they are still warnings. What I want to say is that we should either throw exception for all cases or just use warnings. To be honest the only ones that should be probably converted if we stick with warnings are the int overflows (although you could still argue that the type is not really wrong from PHP point of view - it's just the conversion to the OpenSSL expected type but it's a bit edge case so exception probably makes more sense).

Considering how big break it would be to change everything to exception and the fact that the resource to objects is already quite a big break, it might be best to keep warnings. I think it would be better to later create a new objective API (methods on the classes that were added) and use exceptions for everything in there.

if (key == NULL) {
if (!EG(exception)) {
php_error_docref(NULL, E_WARNING, "Cannot get key from parameter 1");
// TypeError?
zend_argument_value_error(1, "cannot retrieve key");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I drop this ?

if (key == NULL) {
if (!EG(exception)) {
php_error_docref(NULL, E_WARNING, "Cannot get key from parameter 1");
zend_argument_value_error(1, "cannot retrieve key");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoud I drop this?

ext/openssl/openssl.c Outdated Show resolved Hide resolved
ext/openssl/openssl.c Outdated Show resolved Hide resolved
ext/openssl/openssl.c Outdated Show resolved Hide resolved
@@ -3563,7 +3564,7 @@ static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *pas
/* get passphrase */

if ((zphrase = zend_hash_index_find(Z_ARRVAL_P(val), 1)) == NULL) {
php_error_docref(NULL, E_WARNING, "Key array must be of the form array(0 => key, 1 => phrase)");
zend_value_error("Key array must be of the form array(0 => key, 1 => phrase)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be inconsistent with other checks - please change to warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is incorrectly formatted array, it should be fine to keep it as error.

@@ -3582,7 +3583,7 @@ static EVP_PKEY *php_openssl_pkey_from_zval(zval *val, int public_key, char *pas

/* now set val to be the key param and continue */
if ((val = zend_hash_index_find(Z_ARRVAL_P(val), 0)) == NULL) {
php_error_docref(NULL, E_WARNING, "Key array must be of the form array(0 => key, 1 => phrase)");
zend_value_error("Key array must be of the form array(0 => key, 1 => phrase)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be inconsistent with other checks - please change to warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is incorrectly formatted array, it should be fine to keep it as error.

@@ -3709,8 +3710,7 @@ static EVP_PKEY * php_openssl_generate_private_key(struct php_x509_request * req
EVP_PKEY * return_val = NULL;

if (req->priv_key_bits < MIN_KEY_LENGTH) {
php_error_docref(NULL, E_WARNING, "Private key length is too short; it needs to be at least %d bits, not %d",
MIN_KEY_LENGTH, req->priv_key_bits);
zend_value_error("Private key must be at least %d bits", MIN_KEY_LENGTH);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be inconsistent with other checks - please change to warning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We changed a bunch of minimal values to use ValueError in other extensions while still keeping E_WARNINGs for other cases. Same goes for the empty case for strings/arrays and I don't exactly see why OpenSSL should get a pass on this to keep everything as warnings.
ValueErrors are there to point out programming errors, I understand why the signature or the coercion ones need to be kept as Warnings as they might be due to the OpenSSL version/file environment, but here I don't see why it should be kept a warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this comes from openssl configuration (e.g. openssl.cnf or system config) so it's not really a programming error but more configuration error. Basically it's not like user passed different value.

php_error_docref(NULL, E_WARNING,
"Cipher algorithm requires an IV to be supplied as a sixth parameter");
RETURN_FALSE;
zend_argument_value_error(6, "must provide an IV for chosen cipher algorithm");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be inconsistent with other checks - please change to warning.

}
if ((size_t)cipher_iv_len != iv_len) {
php_error_docref(NULL, E_WARNING, "IV length is invalid");
RETURN_FALSE;
zend_argument_value_error(6, "length is invalid");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be inconsistent with other checks - please change to warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still stands - it should be put back to warning.

@@ -7429,10 +7438,11 @@ PHP_FUNCTION(openssl_cipher_iv_length)
}

if (!method_len) {
php_error_docref(NULL, E_WARNING, "Unknown cipher algorithm");
RETURN_FALSE;
zend_argument_value_error(1, "cannot be empty");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be inconsistent with other checks - please change to warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess empty string could be still kept as error so this is fine.

@@ -4786,7 +4787,8 @@ PHP_FUNCTION(openssl_pkey_derive)
RETURN_THROWS();
}
if (key_len < 0) {
php_error_docref(NULL, E_WARNING, "keylen < 0, assuming NULL");
zend_argument_value_error(3, "must be greater than or equal to 0");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be inconsistent with other checks - please change to warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok you can use actually error in here as it's a programming error so ignore my comment

@nikic
Copy link
Member

nikic commented Aug 4, 2020

@bukka For us it is important that throwing of errors is consistent across PHP, not within the OpenSSL extension. It is normal and expected that a function can throw both warnings and Error exceptions depending on the type of error condition that is encountered (all functions throw TypeError at the very least). If an error condition indicates a programming error, it must throw an Error exception, otherwise it must throw a warning (or a normal Exception). Whether there are other warnings in the same function doesn't factor into the decision process.

@bukka
Copy link
Member

bukka commented Aug 4, 2020

@nikic Then you should throw error for all cases that indicate programming error. Having just some throw error and then exactly the same one for another parameter not throwing error seems like a really bad solution to me and it's completely inconsistent.

@bukka
Copy link
Member

bukka commented Aug 4, 2020

Basically then user will have to factor for both cases which is not good and no one then can really say what throws exception and what doesn't unless looking to the source code. If you want to keep some warnings then you need to draw a line and I'm not really sure from this PR where that line is.

@bukka
Copy link
Member

bukka commented Aug 4, 2020

I'm fine with few more cases to stay as errors (invalid array format and negative key len). Others are really overlapping with the same values not being error. For example wrong algorithm is in some way like invalid CMS envelope because that can be invalid if there's invalid algorithm. Both are in some way programming errors and if we change one, we should change another one. But then it can kind of means changing almost everything...

@bukka
Copy link
Member

bukka commented Aug 9, 2020

@Girgias It looks mostly good. I think that IV length checks should be put back as warning. The reason is that IV length is dependent on algorithm and if algorithm is warning, then IV should should be too. That kind of say that empty IV is still variant of valid IV length (empty IV is valid for ECB mode even though ECB mode is in general a good idea it's still valid) so it should be still warning in my thinking.

@Girgias
Copy link
Member Author

Girgias commented Aug 9, 2020

@Girgias It looks mostly good. I think that IV length checks should be put back as warning. The reason is that IV length is dependent on algorithm and if algorithm is warning, then IV should should be too. That kind of say that empty IV is still variant of valid IV length (empty IV is valid for ECB mode even though ECB mode is in general a good idea it's still valid) so it should be still warning in my thinking.

Makes sense will revert that then

@bukka
Copy link
Member

bukka commented Aug 16, 2020

@Girgias It looks good to me now.

@php-pulls php-pulls closed this in 4522cbb Aug 16, 2020
@Girgias Girgias deleted the openssl-warning-promotion branch August 16, 2020 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants