x509: Replace fixed acert group string getter by a more dynamic group enumerator
This commit is contained in:
@@ -31,8 +31,9 @@
|
|||||||
#include <credentials/certificates/ac.h>
|
#include <credentials/certificates/ac.h>
|
||||||
#include <credentials/certificates/crl.h>
|
#include <credentials/certificates/crl.h>
|
||||||
#include <credentials/certificates/pgp_certificate.h>
|
#include <credentials/certificates/pgp_certificate.h>
|
||||||
#include <credentials/ietf_attributes/ietf_attributes.h>
|
|
||||||
#include <config/peer_cfg.h>
|
#include <config/peer_cfg.h>
|
||||||
|
#include <asn1/asn1.h>
|
||||||
|
#include <asn1/oid.h>
|
||||||
|
|
||||||
/* warning intervals for list functions */
|
/* warning intervals for list functions */
|
||||||
#define CERT_WARNING_INTERVAL 30 /* days */
|
#define CERT_WARNING_INTERVAL 30 /* days */
|
||||||
@@ -1027,16 +1028,19 @@ static void stroke_list_certs(linked_list_t *list, char *label,
|
|||||||
static void stroke_list_acerts(linked_list_t *list, bool utc, FILE *out)
|
static void stroke_list_acerts(linked_list_t *list, bool utc, FILE *out)
|
||||||
{
|
{
|
||||||
bool first = TRUE;
|
bool first = TRUE;
|
||||||
time_t thisUpdate, nextUpdate, now = time(NULL);
|
time_t notBefore, notAfter, now = time(NULL);
|
||||||
enumerator_t *enumerator = list->create_enumerator(list);
|
enumerator_t *enumerator;
|
||||||
certificate_t *cert;
|
certificate_t *cert;
|
||||||
|
|
||||||
while (enumerator->enumerate(enumerator, (void**)&cert))
|
enumerator = list->create_enumerator(list);
|
||||||
|
while (enumerator->enumerate(enumerator, &cert))
|
||||||
{
|
{
|
||||||
ac_t *ac = (ac_t*)cert;
|
ac_t *ac = (ac_t*)cert;
|
||||||
|
ac_group_type_t type;
|
||||||
identification_t *id;
|
identification_t *id;
|
||||||
ietf_attributes_t *groups;
|
enumerator_t *groups;
|
||||||
chunk_t chunk;
|
chunk_t chunk;
|
||||||
|
bool firstgroup = TRUE;
|
||||||
|
|
||||||
if (first)
|
if (first)
|
||||||
{
|
{
|
||||||
@@ -1061,30 +1065,78 @@ static void stroke_list_acerts(linked_list_t *list, bool utc, FILE *out)
|
|||||||
{
|
{
|
||||||
fprintf(out, " hserial: %#B\n", &chunk);
|
fprintf(out, " hserial: %#B\n", &chunk);
|
||||||
}
|
}
|
||||||
groups = ac->get_groups(ac);
|
groups = ac->create_group_enumerator(ac);
|
||||||
if (groups)
|
while (groups->enumerate(groups, &type, &chunk))
|
||||||
{
|
{
|
||||||
fprintf(out, " groups: %s\n", groups->get_string(groups));
|
int oid;
|
||||||
groups->destroy(groups);
|
char *str;
|
||||||
|
|
||||||
|
if (firstgroup)
|
||||||
|
{
|
||||||
|
fprintf(out, " groups: ");
|
||||||
|
firstgroup = FALSE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fprintf(out, " ");
|
||||||
|
}
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case AC_GROUP_TYPE_STRING:
|
||||||
|
fprintf(out, "%.*s", (int)chunk.len, chunk.ptr);
|
||||||
|
break;
|
||||||
|
case AC_GROUP_TYPE_OID:
|
||||||
|
oid = asn1_known_oid(chunk);
|
||||||
|
if (oid == OID_UNKNOWN)
|
||||||
|
{
|
||||||
|
str = asn1_oid_to_string(chunk);
|
||||||
|
if (str)
|
||||||
|
{
|
||||||
|
fprintf(out, "%s", str);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fprintf(out, "OID:%#B", &chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fprintf(out, "%s", oid_names[oid].name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AC_GROUP_TYPE_OCTETS:
|
||||||
|
fprintf(out, "%#B", &chunk);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fprintf(out, "\n");
|
||||||
}
|
}
|
||||||
|
groups->destroy(groups);
|
||||||
fprintf(out, " issuer: \"%Y\"\n", cert->get_issuer(cert));
|
fprintf(out, " issuer: \"%Y\"\n", cert->get_issuer(cert));
|
||||||
chunk = chunk_skip_zero(ac->get_serial(ac));
|
chunk = chunk_skip_zero(ac->get_serial(ac));
|
||||||
fprintf(out, " serial: %#B\n", &chunk);
|
fprintf(out, " serial: %#B\n", &chunk);
|
||||||
|
|
||||||
/* list validity */
|
/* list validity */
|
||||||
cert->get_validity(cert, &now, &thisUpdate, &nextUpdate);
|
cert->get_validity(cert, &now, ¬Before, ¬After);
|
||||||
fprintf(out, " updates: this %T\n", &thisUpdate, utc);
|
fprintf(out, " validity: not before %T, ", ¬Before, utc);
|
||||||
fprintf(out, " next %T, ", &nextUpdate, utc);
|
if (now < notBefore)
|
||||||
if (now > nextUpdate)
|
|
||||||
{
|
{
|
||||||
fprintf(out, "expired (%V ago)\n", &now, &nextUpdate);
|
fprintf(out, "not valid yet (valid in %V)\n", &now, ¬Before);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fprintf(out, "ok\n");
|
||||||
|
}
|
||||||
|
fprintf(out, " not after %T, ", ¬After, utc);
|
||||||
|
if (now > notAfter)
|
||||||
|
{
|
||||||
|
fprintf(out, "expired (%V ago)\n", &now, ¬After);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
fprintf(out, "ok");
|
fprintf(out, "ok");
|
||||||
if (now > nextUpdate - AC_WARNING_INTERVAL * 60 * 60 * 24)
|
if (now > notAfter - AC_WARNING_INTERVAL * 60 * 60 * 24)
|
||||||
{
|
{
|
||||||
fprintf(out, " (expires in %V)", &now, &nextUpdate);
|
fprintf(out, " (expires in %V)", &now, ¬After);
|
||||||
}
|
}
|
||||||
fprintf(out, " \n");
|
fprintf(out, " \n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,16 @@
|
|||||||
#include <credentials/ietf_attributes/ietf_attributes.h>
|
#include <credentials/ietf_attributes/ietf_attributes.h>
|
||||||
|
|
||||||
typedef struct ac_t ac_t;
|
typedef struct ac_t ac_t;
|
||||||
|
typedef enum ac_group_type_t ac_group_type_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common group types, from IETF Attributes Syntax
|
||||||
|
*/
|
||||||
|
enum ac_group_type_t {
|
||||||
|
AC_GROUP_TYPE_OCTETS,
|
||||||
|
AC_GROUP_TYPE_STRING,
|
||||||
|
AC_GROUP_TYPE_OID,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* X.509 attribute certificate interface.
|
* X.509 attribute certificate interface.
|
||||||
@@ -70,11 +80,11 @@ struct ac_t {
|
|||||||
chunk_t (*get_authKeyIdentifier)(ac_t *this);
|
chunk_t (*get_authKeyIdentifier)(ac_t *this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the group memberships as a list of IETF attributes
|
* Create an enumerator of contained Group memberships.
|
||||||
*
|
*
|
||||||
* @return object containing a list of IETF attributes
|
* @return enumerator over (ac_group_type_t, chunk_t)
|
||||||
*/
|
*/
|
||||||
ietf_attributes_t* (*get_groups)(ac_t *this);
|
enumerator_t* (*create_group_enumerator)(ac_t *this);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /** AC_H_ @}*/
|
#endif /** AC_H_ @}*/
|
||||||
|
|||||||
@@ -19,49 +19,26 @@
|
|||||||
#include <asn1/asn1_parser.h>
|
#include <asn1/asn1_parser.h>
|
||||||
#include <collections/linked_list.h>
|
#include <collections/linked_list.h>
|
||||||
#include <utils/lexparser.h>
|
#include <utils/lexparser.h>
|
||||||
|
#include <credentials/certificates/ac.h>
|
||||||
|
|
||||||
#include "ietf_attributes.h"
|
#include "ietf_attributes.h"
|
||||||
|
|
||||||
/**
|
|
||||||
* Private definition of IETF attribute types
|
|
||||||
*/
|
|
||||||
typedef enum {
|
|
||||||
IETF_ATTRIBUTE_OCTETS = 0,
|
|
||||||
IETF_ATTRIBUTE_OID = 1,
|
|
||||||
IETF_ATTRIBUTE_STRING = 2
|
|
||||||
} ietf_attribute_type_t;
|
|
||||||
|
|
||||||
typedef struct ietf_attr_t ietf_attr_t;
|
typedef struct ietf_attr_t ietf_attr_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private definition of an IETF attribute
|
* Private definition of an IETF attribute
|
||||||
*/
|
*/
|
||||||
struct ietf_attr_t {
|
struct ietf_attr_t {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IETF attribute type
|
* IETF attribute type
|
||||||
*/
|
*/
|
||||||
ietf_attribute_type_t type;
|
ac_group_type_t type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IETF attribute value
|
* IETF attribute value
|
||||||
*/
|
*/
|
||||||
chunk_t value;
|
chunk_t value;
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two IETF attributes
|
|
||||||
*
|
|
||||||
* return -1 if this is earlier in the alphabet than other
|
|
||||||
* return 0 if this equals other
|
|
||||||
* return +1 if this is later in the alphabet than other
|
|
||||||
*
|
|
||||||
* @param other other object
|
|
||||||
*/
|
|
||||||
int (*compare) (ietf_attr_t *this, ietf_attr_t *other);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroys an ietf_attr_t object.
|
|
||||||
*/
|
|
||||||
void (*destroy) (ietf_attr_t *this);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,11 +49,11 @@ static int ietf_attr_compare(ietf_attr_t *this, ietf_attr_t *other)
|
|||||||
int cmp_len, len, cmp_value;
|
int cmp_len, len, cmp_value;
|
||||||
|
|
||||||
/* OID attributes are appended after STRING and OCTETS attributes */
|
/* OID attributes are appended after STRING and OCTETS attributes */
|
||||||
if (this->type != IETF_ATTRIBUTE_OID && other->type == IETF_ATTRIBUTE_OID)
|
if (this->type != AC_GROUP_TYPE_OID && other->type == AC_GROUP_TYPE_OID)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (this->type == IETF_ATTRIBUTE_OID && other->type != IETF_ATTRIBUTE_OID)
|
if (this->type == AC_GROUP_TYPE_OID && other->type != AC_GROUP_TYPE_OID)
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -100,13 +77,11 @@ static void ietf_attr_destroy(ietf_attr_t *this)
|
|||||||
/**
|
/**
|
||||||
* Creates an ietf_attr_t object.
|
* Creates an ietf_attr_t object.
|
||||||
*/
|
*/
|
||||||
static ietf_attr_t* ietf_attr_create(ietf_attribute_type_t type, chunk_t value)
|
static ietf_attr_t* ietf_attr_create(ac_group_type_t type, chunk_t value)
|
||||||
{
|
{
|
||||||
ietf_attr_t *this;
|
ietf_attr_t *this;
|
||||||
|
|
||||||
INIT(this,
|
INIT(this,
|
||||||
.compare = ietf_attr_compare,
|
|
||||||
.destroy = ietf_attr_destroy,
|
|
||||||
.type = type,
|
.type = type,
|
||||||
.value = chunk_clone(value),
|
.value = chunk_clone(value),
|
||||||
);
|
);
|
||||||
@@ -175,12 +150,12 @@ METHOD(ietf_attributes_t, get_string, char*,
|
|||||||
|
|
||||||
switch (attr->type)
|
switch (attr->type)
|
||||||
{
|
{
|
||||||
case IETF_ATTRIBUTE_OCTETS:
|
case AC_GROUP_TYPE_OCTETS:
|
||||||
case IETF_ATTRIBUTE_STRING:
|
case AC_GROUP_TYPE_STRING:
|
||||||
written = snprintf(pos, len, "%.*s", (int)attr->value.len,
|
written = snprintf(pos, len, "%.*s", (int)attr->value.len,
|
||||||
attr->value.ptr);
|
attr->value.ptr);
|
||||||
break;
|
break;
|
||||||
case IETF_ATTRIBUTE_OID:
|
case AC_GROUP_TYPE_OID:
|
||||||
{
|
{
|
||||||
int oid = asn1_known_oid(attr->value);
|
int oid = asn1_known_oid(attr->value);
|
||||||
|
|
||||||
@@ -214,6 +189,24 @@ METHOD(ietf_attributes_t, get_string, char*,
|
|||||||
return this->string;
|
return this->string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter function for attribute enumeration
|
||||||
|
*/
|
||||||
|
static bool attr_filter(void *null, ietf_attr_t **in, ac_group_type_t *type,
|
||||||
|
void *in2, chunk_t *out)
|
||||||
|
{
|
||||||
|
*type = (*in)->type;
|
||||||
|
*out = (*in)->value;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
METHOD(ietf_attributes_t, create_enumerator, enumerator_t*,
|
||||||
|
private_ietf_attributes_t *this)
|
||||||
|
{
|
||||||
|
return enumerator_create_filter(this->list->create_enumerator(this->list),
|
||||||
|
(void*)attr_filter, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
METHOD(ietf_attributes_t, get_encoding, chunk_t,
|
METHOD(ietf_attributes_t, get_encoding, chunk_t,
|
||||||
private_ietf_attributes_t *this)
|
private_ietf_attributes_t *this)
|
||||||
{
|
{
|
||||||
@@ -243,13 +236,13 @@ METHOD(ietf_attributes_t, get_encoding, chunk_t,
|
|||||||
|
|
||||||
switch (attr->type)
|
switch (attr->type)
|
||||||
{
|
{
|
||||||
case IETF_ATTRIBUTE_OCTETS:
|
case AC_GROUP_TYPE_OCTETS:
|
||||||
type = ASN1_OCTET_STRING;
|
type = ASN1_OCTET_STRING;
|
||||||
break;
|
break;
|
||||||
case IETF_ATTRIBUTE_STRING:
|
case AC_GROUP_TYPE_STRING:
|
||||||
type = ASN1_UTF8STRING;
|
type = ASN1_UTF8STRING;
|
||||||
break;
|
break;
|
||||||
case IETF_ATTRIBUTE_OID:
|
case AC_GROUP_TYPE_OID:
|
||||||
type = ASN1_OID;
|
type = ASN1_OID;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -290,7 +283,7 @@ static bool equals(private_ietf_attributes_t *this,
|
|||||||
while (enum_a->enumerate(enum_a, &attr_a) &&
|
while (enum_a->enumerate(enum_a, &attr_a) &&
|
||||||
enum_b->enumerate(enum_b, &attr_b))
|
enum_b->enumerate(enum_b, &attr_b))
|
||||||
{
|
{
|
||||||
if (attr_a->compare(attr_a, attr_b) != 0)
|
if (ietf_attr_compare(attr_a, attr_b) != 0)
|
||||||
{
|
{
|
||||||
/* we have a mismatch */
|
/* we have a mismatch */
|
||||||
result = FALSE;
|
result = FALSE;
|
||||||
@@ -334,8 +327,9 @@ static bool matches(private_ietf_attributes_t *this,
|
|||||||
/* look for at least one common attribute */
|
/* look for at least one common attribute */
|
||||||
while (TRUE)
|
while (TRUE)
|
||||||
{
|
{
|
||||||
int cmp = attr_a->compare(attr_a, attr_b);
|
int cmp;
|
||||||
|
|
||||||
|
cmp = ietf_attr_compare(attr_a, attr_b);
|
||||||
if (cmp == 0)
|
if (cmp == 0)
|
||||||
{
|
{
|
||||||
/* we have a match */
|
/* we have a match */
|
||||||
@@ -379,7 +373,7 @@ METHOD(ietf_attributes_t, destroy, void,
|
|||||||
{
|
{
|
||||||
if (ref_put(&this->ref))
|
if (ref_put(&this->ref))
|
||||||
{
|
{
|
||||||
this->list->destroy_offset(this->list, offsetof(ietf_attr_t, destroy));
|
this->list->destroy_function(this->list, (void*)ietf_attr_destroy);
|
||||||
free(this->string);
|
free(this->string);
|
||||||
free(this);
|
free(this);
|
||||||
}
|
}
|
||||||
@@ -392,6 +386,7 @@ static private_ietf_attributes_t* create_empty(void)
|
|||||||
INIT(this,
|
INIT(this,
|
||||||
.public = {
|
.public = {
|
||||||
.get_string = _get_string,
|
.get_string = _get_string,
|
||||||
|
.create_enumerator = _create_enumerator,
|
||||||
.get_encoding = _get_encoding,
|
.get_encoding = _get_encoding,
|
||||||
.equals = (bool (*)(ietf_attributes_t*,ietf_attributes_t*))equals,
|
.equals = (bool (*)(ietf_attributes_t*,ietf_attributes_t*))equals,
|
||||||
.matches = (bool (*)(ietf_attributes_t*,ietf_attributes_t*))matches,
|
.matches = (bool (*)(ietf_attributes_t*,ietf_attributes_t*))matches,
|
||||||
@@ -417,13 +412,13 @@ static void ietf_attributes_add(private_ietf_attributes_t *this,
|
|||||||
|
|
||||||
enumerator = this->list->create_enumerator(this->list);
|
enumerator = this->list->create_enumerator(this->list);
|
||||||
while (enumerator->enumerate(enumerator, (void **)¤t_attr) &&
|
while (enumerator->enumerate(enumerator, (void **)¤t_attr) &&
|
||||||
(cmp = attr->compare(attr, current_attr)) > 0)
|
(cmp = ietf_attr_compare(attr, current_attr)) > 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (cmp == 0)
|
if (cmp == 0)
|
||||||
{
|
{
|
||||||
attr->destroy(attr);
|
ietf_attr_destroy(attr);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{ /* the enumerator either points to the end or to the attribute > attr */
|
{ /* the enumerator either points to the end or to the attribute > attr */
|
||||||
@@ -461,7 +456,7 @@ ietf_attributes_t *ietf_attributes_create_from_string(char *string)
|
|||||||
/* add the group attribute to the list */
|
/* add the group attribute to the list */
|
||||||
if (group.len > 0)
|
if (group.len > 0)
|
||||||
{
|
{
|
||||||
ietf_attr_t *attr = ietf_attr_create(IETF_ATTRIBUTE_STRING, group);
|
ietf_attr_t *attr = ietf_attr_create(AC_GROUP_TYPE_STRING, group);
|
||||||
|
|
||||||
ietf_attributes_add(this, attr);
|
ietf_attributes_add(this, attr);
|
||||||
}
|
}
|
||||||
@@ -515,7 +510,7 @@ ietf_attributes_t *ietf_attributes_create_from_encoding(chunk_t encoded)
|
|||||||
case IETF_ATTR_OID:
|
case IETF_ATTR_OID:
|
||||||
case IETF_ATTR_STRING:
|
case IETF_ATTR_STRING:
|
||||||
{
|
{
|
||||||
ietf_attribute_type_t type;
|
ac_group_type_t type;
|
||||||
ietf_attr_t *attr;
|
ietf_attr_t *attr;
|
||||||
|
|
||||||
type = (objectID - IETF_ATTR_OCTETS) / 2;
|
type = (objectID - IETF_ATTR_OCTETS) / 2;
|
||||||
@@ -531,4 +526,3 @@ ietf_attributes_t *ietf_attributes_create_from_encoding(chunk_t encoded)
|
|||||||
|
|
||||||
return &(this->public);
|
return &(this->public);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ struct ietf_attributes_t {
|
|||||||
*/
|
*/
|
||||||
char* (*get_string) (ietf_attributes_t *this);
|
char* (*get_string) (ietf_attributes_t *this);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an enumerator over attributes.
|
||||||
|
*
|
||||||
|
* @return enumerator over (ac_group_type_t, chunk_t)
|
||||||
|
*/
|
||||||
|
enumerator_t* (*create_enumerator)(ietf_attributes_t *this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ASN.1 encoding of the IETF attributes.
|
* Get the ASN.1 encoding of the IETF attributes.
|
||||||
*
|
*
|
||||||
@@ -89,4 +96,3 @@ ietf_attributes_t *ietf_attributes_create_from_string(char *string);
|
|||||||
ietf_attributes_t *ietf_attributes_create_from_encoding(chunk_t encoded);
|
ietf_attributes_t *ietf_attributes_create_from_encoding(chunk_t encoded);
|
||||||
|
|
||||||
#endif /** IETF_ATTRIBUTES_H_ @}*/
|
#endif /** IETF_ATTRIBUTES_H_ @}*/
|
||||||
|
|
||||||
|
|||||||
@@ -655,10 +655,10 @@ METHOD(ac_t, get_authKeyIdentifier, chunk_t,
|
|||||||
return this->authKeyIdentifier;
|
return this->authKeyIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
METHOD(ac_t, get_groups, ietf_attributes_t*,
|
METHOD(ac_t, create_group_enumerator, enumerator_t*,
|
||||||
private_x509_ac_t *this)
|
private_x509_ac_t *this)
|
||||||
{
|
{
|
||||||
return this->groups ? this->groups->get_ref(this->groups) : NULL;
|
return this->groups->create_enumerator(this->groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
METHOD(certificate_t, get_type, certificate_type_t,
|
METHOD(certificate_t, get_type, certificate_type_t,
|
||||||
@@ -866,7 +866,7 @@ static private_x509_ac_t *create_empty(void)
|
|||||||
.get_holderSerial = _get_holderSerial,
|
.get_holderSerial = _get_holderSerial,
|
||||||
.get_holderIssuer = _get_holderIssuer,
|
.get_holderIssuer = _get_holderIssuer,
|
||||||
.get_authKeyIdentifier = _get_authKeyIdentifier,
|
.get_authKeyIdentifier = _get_authKeyIdentifier,
|
||||||
.get_groups = _get_groups,
|
.create_group_enumerator = _create_group_enumerator,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.ref = 1,
|
.ref = 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user