Hello,

This patch set contains necessary changes for supporting root master zone in LDAP. I had to remove one hack so now we follow BIND semantics for forwarders.


Please see commit messages.

https://fedorahosted.org/bind-dyndb-ldap/ticket/122

--
Petr^2 Spacek
From 21f7ecd4eb4f977392975034fc9de3b61958e2aa Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 6 Jun 2014 19:28:13 +0200
Subject: [PATCH] Refactor DN->DNS name conversion.

Original code was fragile and unable to parse name of root zone
(idnsName=.).

https://fedorahosted.org/bind-dyndb-ldap/ticket/122

Signed-off-by: Petr Spacek <pspa...@redhat.com>
---
 src/ldap_convert.c | 275 +++++++++++++++++++++--------------------------------
 1 file changed, 106 insertions(+), 169 deletions(-)

diff --git a/src/ldap_convert.c b/src/ldap_convert.c
index 89c371d27447b34e28bd24ddcaefb58fcaf1dd07..be5c2e1d4dc903b4d9e72cc07ed1d9fc32fef0d1 100644
--- a/src/ldap_convert.c
+++ b/src/ldap_convert.c
@@ -40,164 +40,149 @@
 #include "util.h"
 #include "zone_register.h"
 
-static isc_result_t explode_dn(const char *dn, char ***explodedp, int notypes) ATTR_NONNULLS ATTR_CHECKRESULT;
-static isc_result_t explode_rdn(const char *rdn, char ***explodedp,
-				int notypes) ATTR_NONNULLS ATTR_CHECKRESULT;
-
-
-isc_result_t
-dn_to_dnsname(isc_mem_t *mctx, const char *dn, dns_name_t *target,
-	      dns_name_t *otarget)
-{
-	isc_result_t result;
-	DECLARE_BUFFERED_NAME(name);
-	DECLARE_BUFFERED_NAME(origin);
-	ld_string_t *str = NULL;
-	ld_string_t *ostr = NULL;
-	isc_buffer_t buffer;
-
-	REQUIRE(dn != NULL);
-
-	INIT_BUFFERED_NAME(name);
-	CHECK(str_new(mctx, &str));
-
-	if (otarget != NULL) {
-		INIT_BUFFERED_NAME(origin);
-		CHECK(str_new(mctx, &ostr));
-	}
-
-	CHECK(dn_to_text(dn, str, ostr));
-	str_to_isc_buffer(str, &buffer);
-	CHECK(dns_name_fromtext(&name, &buffer, NULL, 0, NULL));
-
-	if (otarget != NULL) {
-		str_to_isc_buffer(ostr, &buffer);
-		CHECK(dns_name_fromtext(&origin, &buffer, NULL, 0, NULL));
-	}
-
-cleanup:
-	if (result == ISC_R_SUCCESS)
-		result = dns_name_dupwithoffsets(&name, mctx, target);
-	else
-		log_error_r("failed to convert dn %s to DNS name", dn);
-
-	if (otarget != NULL && result == ISC_R_SUCCESS)
-		result = dns_name_dupwithoffsets(&origin, mctx, otarget);
-
-	if (result != ISC_R_SUCCESS) {
-		if (dns_name_dynamic(target))
-			dns_name_free(target, mctx);
-		if (otarget) {
-			if (dns_name_dynamic(otarget))
-				dns_name_free(otarget, mctx);
-		}
-	}
-
-	str_destroy(&str);
-	if (otarget != NULL)
-		str_destroy(&ostr);
-
-	return result;
-}
-
 /**
- * Convert LDAP DN to absolute DNS name.
+ * Convert LDAP DN to absolute DNS names.
  *
  * @param[in]  dn     LDAP DN with one or two idnsName components at the
  *                    beginning.
- * @param[out] target Absolute DNS name derived from the all idnsNames.
+ * @param[out] target Absolute DNS name derived from the first two idnsNames.
  * @param[out] origin Absolute DNS name derived from the last idnsName
  *                    component of DN, i.e. zone. Can be NULL.
  *
  * @code
  * Examples:
- * dn = "idnsName=foo, idnsName=bar, idnsName=example.org,"
- *      "cn=dns, dc=example, dc=org"
+ * dn = "idnsName=foo.bar, idnsName=example.org., cn=dns, dc=example, dc=org"
  * target = "foo.bar.example.org."
  * origin = "example.org."
  *
- * dn = "idnsname=89, idnsname=4.34.10.in-addr.arpa.",
- *      " cn=dns, dc=example, dc=org"
+ * dn = "idnsname=89, idnsname=4.34.10.in-addr.arpa, cn=dns, dc=example, dc=org"
  * target = "89.4.34.10.in-addr.arpa."
  * origin = "4.34.10.in-addr.arpa."
- * (The dot at the end is not doubled when it's already present.)
+ *
+ * dn = "idnsname=third.test., idnsname=test., cn=dns, dc=example, dc=org"
+ * target = "third.test."
+ * origin = "test."
  * @endcode
  */
 isc_result_t
-dn_to_text(const char *dn, ld_string_t *target, ld_string_t *origin)
+dn_to_dnsname(isc_mem_t *mctx, const char *dn_str, dns_name_t *target,
+	      dns_name_t *otarget)
 {
+	LDAPDN dn = NULL;
+	LDAPRDN rdn = NULL;
+	LDAPAVA *attr = NULL;
+	int idx;
+	int ret;
+
+	DECLARE_BUFFERED_NAME(name);
+	DECLARE_BUFFERED_NAME(origin);
+	isc_buffer_t name_buf;
+	isc_buffer_t origin_buf;
 	isc_result_t result;
-	char **exploded_dn = NULL;
-	char **exploded_rdn = NULL;
-	unsigned int i;
 
-	REQUIRE(dn != NULL);
+	REQUIRE(dn_str != NULL);
 	REQUIRE(target != NULL);
 
-	CHECK(explode_dn(dn, &exploded_dn, 0));
-	str_clear(target);
-	for (i = 0; exploded_dn[i] != NULL; i++) {
-		if (strncasecmp(exploded_dn[i], "idnsName", 8) != 0)
+	INIT_BUFFERED_NAME(name);
+	INIT_BUFFERED_NAME(origin);
+	isc_buffer_initnull(&name_buf);
+	isc_buffer_initnull(&origin_buf);
+
+	/* Example DN: cn=a+sn=b, ou=people */
+
+	ret = ldap_str2dn(dn_str, &dn, LDAP_DN_FORMAT_LDAPV3);
+	if (ret != LDAP_SUCCESS || dn == NULL) {
+		log_bug("ldap_str2dn failed: %u", ret);
+		CLEANUP_WITH(ISC_R_UNEXPECTED);
+	}
+
+	/* iterate over DN components: e.g. cn=a+sn=b */
+	for (idx = 0; dn[idx] != NULL; idx++) {
+		rdn = dn[idx];
+
+		/* "iterate" over RDN components: e.g. cn=a */
+		INSIST(rdn[0] != NULL); /* RDN without (attr=value)?! */
+		if (rdn[1] != NULL) {
+			log_bug("multi-valued RDNs are not supported");
+			CLEANUP_WITH(ISC_R_NOTIMPLEMENTED);
+		}
+
+		/* attribute in current RDN component */
+		attr = rdn[0];
+		if ((attr->la_flags & LDAP_AVA_STRING) == 0) {
+			log_error("non-string attribute detected: position %u",
+				  idx);
+			CLEANUP_WITH(ISC_R_NOTIMPLEMENTED);
+		}
+
+		if (strncasecmp("idnsName", attr->la_attr.bv_val,
+				attr->la_attr.bv_len) == 0) {
+			if (idx == 0) {
+				isc_buffer_init(&name_buf,
+						attr->la_value.bv_val,
+						attr->la_value.bv_len);
+				isc_buffer_add(&name_buf,
+					       attr->la_value.bv_len);
+			} else if (idx == 1) {
+				isc_buffer_init(&origin_buf,
+						attr->la_value.bv_val,
+						attr->la_value.bv_len);
+				isc_buffer_add(&origin_buf,
+					       attr->la_value.bv_len);
+			} else { /* more than two idnsNames?! */
+				break;
+			}
+		} else { /* no match - idx holds position */
 			break;
-
-		if (exploded_rdn != NULL) {
-			ldap_value_free(exploded_rdn);
-			exploded_rdn = NULL;
 		}
-
-		CHECK(explode_rdn(exploded_dn[i], &exploded_rdn, 1));
-		if (exploded_rdn[0] == NULL || exploded_rdn[1] != NULL) {
-			log_error("idnsName component of DN has to have "
-				  "exactly one value: DN '%s'", dn);
-			CLEANUP_WITH(ISC_R_NOTIMPLEMENTED);
-		}
-		CHECK(str_cat_char(target, exploded_rdn[0]));
-		if (str_buf(target)[str_len(target)-1] != '.')
-			CHECK(str_cat_char(target, "."));
 	}
 
 	/* filter out unsupported cases */
-	if (i <= 0) {
-		log_error("no idnsName component found in DN '%s'", dn);
+	if (idx <= 0) {
+		log_error("no idnsName component found in DN");
 		CLEANUP_WITH(ISC_R_UNEXPECTEDEND);
-	} else if (i == 1) { /* zone only - nothing to check */
-		;
-	} else if (i == 2) {
-		if (exploded_dn[0][strlen(exploded_dn[0])-1] == '.') {
-			log_error("absolute record name in DN "
-				  "is not supported: DN '%s'", dn);
-			CLEANUP_WITH(ISC_R_NOTIMPLEMENTED);
+	} else if (idx == 1) { /* zone only */
+		CHECK(dns_name_copy(dns_rootname, &origin, NULL));
+		CHECK(dns_name_fromtext(&name, &name_buf, dns_rootname, 0, NULL));
+	} else if (idx == 2) { /* owner and zone */
+		CHECK(dns_name_fromtext(&origin, &origin_buf, dns_rootname, 0,
+					NULL));
+		CHECK(dns_name_fromtext(&name, &name_buf, &origin, 0, NULL));
+		if (dns_name_issubdomain(&name, &origin) == ISC_FALSE) {
+			log_error("out-of-zone data: first idnsName is not a "
+				  "subdomain of the other");
+			CLEANUP_WITH(DNS_R_BADOWNERNAME);
+		} else if (dns_name_equal(&name, &origin) == ISC_TRUE) {
+			log_error("attempt to redefine zone apex: first "
+				  "idnsName equals to zone name");
+			CLEANUP_WITH(DNS_R_BADOWNERNAME);
 		}
 	} else {
-		log_error("unsupported number of idnsName components in DN "
-			  "'%s': %u components found", dn, i);
+		log_error("unsupported number of idnsName components in DN: "
+			  "%u components found", idx);
 		CLEANUP_WITH(ISC_R_NOTIMPLEMENTED);
 	}
 
-	if (origin != NULL) {
-		str_clear(origin);
+cleanup:
+	if (result == ISC_R_SUCCESS)
+		result = dns_name_dupwithoffsets(&name, mctx, target);
+	else
+		log_error_r("failed to convert DN '%s' to DNS name", dn_str);
 
-		/*
-		 * If we have DNs with only one idnsName part,
-		 * treat them as absolute zone name, i.e. origin is root.
-		 */
-		if (i < 2)
-			CHECK(str_init_char(origin, "."));
-		else {
-			CHECK(str_cat_char(origin, exploded_rdn[0]));
-			if (str_buf(origin)[str_len(origin)-1] != '.')
-				CHECK(str_cat_char(origin, "."));
+	if (result == ISC_R_SUCCESS && otarget != NULL)
+		result = dns_name_dupwithoffsets(&origin, mctx, otarget);
+
+	if (result != ISC_R_SUCCESS) {
+		if (dns_name_dynamic(target))
+			dns_name_free(target, mctx);
+		if (otarget) {
+			if (dns_name_dynamic(otarget))
+				dns_name_free(otarget, mctx);
 		}
 	}
 
-	if (str_len(target) == 0)
-		CHECK(str_init_char(target, "."));
-
-cleanup:
-	if (exploded_dn != NULL)
-		ldap_value_free(exploded_dn);
-	if (exploded_rdn != NULL)
-		ldap_value_free(exploded_rdn);
+	if (dn != NULL)
+		ldap_dnfree(dn);
 
 	return result;
 }
@@ -309,54 +294,6 @@ cleanup:
 	return result;
 }
 
-static isc_result_t ATTR_NONNULLS ATTR_CHECKRESULT
-explode_dn(const char *dn, char ***explodedp, int notypes)
-{
-	char **exploded;
-
-	REQUIRE(dn != NULL);
-	REQUIRE(explodedp != NULL && *explodedp == NULL);
-
-	exploded = ldap_explode_dn(dn, notypes);
-	if (exploded == NULL) {
-		if (errno == ENOMEM) {
-			return ISC_R_NOMEMORY;
-		} else {
-			log_error("ldap_explode_dn(\"%s\") failed, "
-				  "error code %d", dn, errno);
-			return ISC_R_FAILURE;
-		}
-	}
-
-	*explodedp = exploded;
-
-	return ISC_R_SUCCESS;
-}
-
-static isc_result_t ATTR_NONNULLS ATTR_CHECKRESULT
-explode_rdn(const char *rdn, char ***explodedp, int notypes)
-{
-	char **exploded;
-
-	REQUIRE(rdn != NULL);
-	REQUIRE(explodedp != NULL && *explodedp == NULL);
-
-	exploded = ldap_explode_rdn(rdn, notypes);
-	if (exploded == NULL) {
-		if (errno == ENOMEM) {
-			return ISC_R_NOMEMORY;
-		} else {
-			log_error("ldap_explode_rdn(\"%s\") failed, "
-				  "error code %d", rdn, errno);
-			return ISC_R_FAILURE;
-		}
-	}
-
-	*explodedp = exploded;
-
-	return ISC_R_SUCCESS;
-}
-
 isc_result_t
 dnsname_to_dn(zone_register_t *zr, dns_name_t *name, ld_string_t *target)
 {
-- 
1.9.3

From 3dbfc515aacc089c1849ae1b85fb8e87c64e95f4 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 6 Jun 2014 19:30:05 +0200
Subject: [PATCH] Support root master zone in LDAP; Follow BIND semantics for
 forwarders.

In this case "support" actually means "remove hack" enforced by mixing
forward/master zones in one object class.

From now on, forwarding configured in LDAP (attributes idnsForward and
idnsForwarders) fully follows BIND behavior for master and forward zones:

- Master zones in LDAP are defined by idnsZone object class.
- Forward zones in LDAP are defined by idnsForward object class.

I.e. idnsForwarders attribute in master zone *does not* change zone type
to forward.

*THIS IS INCOMPATIBLE CHANGE*

https://fedorahosted.org/bind-dyndb-ldap/ticket/122

Signed-off-by: Petr Spacek <pspa...@redhat.com>
---
 src/ldap_helper.c | 22 +++-------------------
 1 file changed, 3 insertions(+), 19 deletions(-)

diff --git a/src/ldap_helper.c b/src/ldap_helper.c
index deda6955a215441a40857d78273fb8042275385e..43bacf779f44a709b0cefd638826633b9d2d8891 100644
--- a/src/ldap_helper.c
+++ b/src/ldap_helper.c
@@ -1397,8 +1397,8 @@ cleanup:
  * @retval others	  Some RBT manipulation errors including ISC_R_FAILURE.
  */
 static isc_result_t ATTR_NONNULLS ATTR_CHECKRESULT
-configure_zone_forwarders(ldap_entry_t *entry, ldap_instance_t *inst, 
-                          dns_name_t *name)
+configure_zone_forwarders(ldap_entry_t *entry, ldap_instance_t *inst,
+			  dns_name_t *name)
 {
 	const char *dn = entry->dn;
 	isc_result_t result;
@@ -2294,25 +2294,9 @@ ldap_parse_master_zoneentry(ldap_entry_t * const entry, dns_db_t * const olddb,
 
 	run_exclusive_enter(task, &lock_state);
 
-	/*
-	 * TODO: Remove this hack, most probably before Fedora 20.
-	 * Forwarding has top priority hence when the forwarders are properly
-	 * set up all others attributes are ignored.
-	 */
 	result = configure_zone_forwarders(entry, inst, &name);
-	if (result != ISC_R_DISABLED) {
-		if (result == ISC_R_SUCCESS) {
-			/* forwarding was enabled for the zone
-			 * => zone type was changed to "forward"
-			 * => delete "master" zone */
-			CHECK(ldap_delete_zone2(inst, &name, ISC_FALSE,
-						ISC_TRUE));
-		}
-		/* DO NOT CHANGE ANYTHING ELSE after forwarders are set up! */
+	if (result != ISC_R_SUCCESS && result != ISC_R_DISABLED)
 		goto cleanup;
-	}
-	/* No forwarders are used. Zone was removed from fwdtable.
-	 * Load the zone. */
 
 	result = ldap_entry_getvalues(entry, "idnsSecInlineSigning", &values);
 	if (result == ISC_R_NOTFOUND || HEAD(values) == NULL)
-- 
1.9.3

From bcad3a5069e4944db11527eadb580c0d3313df82 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Fri, 6 Jun 2014 19:40:39 +0200
Subject: [PATCH] Remove unused str_* functions.

Signed-off-by: Petr Spacek <pspa...@redhat.com>
---
 src/str.c | 145 --------------------------------------------------------------
 src/str.h |  10 -----
 2 files changed, 155 deletions(-)

diff --git a/src/str.c b/src/str.c
index 131bcc047bf858925999ae4128e4cb23ef51c023..c871ad60e9ffd1ce98e4bbe54d5f63f071bc581d 100644
--- a/src/str.c
+++ b/src/str.c
@@ -212,51 +212,6 @@ str_buf(const ld_string_t *src)
 	return src->data;
 }
 
-/*
- * Copy string from src to dest.
- */
-isc_result_t
-str_copy(ld_string_t *dest, const ld_string_t *src)
-{
-	isc_result_t result;
-	size_t len;
-
-	REQUIRE(dest != NULL);
-	REQUIRE(src != NULL);
-
-	if (src->data == NULL)
-            return ISC_R_SUCCESS;
-
-	len = str_len_internal(src);
-	CHECK(str_alloc(dest, len));
-	memcpy(dest->data, src->data, len + 1);
-
-	return ISC_R_SUCCESS;
-
-cleanup:
-	return result;
-}
-
-/*
- * Make a new string and copy src to it.
- */
-isc_result_t
-str_clone(ld_string_t **dest, const ld_string_t *src _STR_MEM_FLARG)
-{
-	isc_result_t result;
-
-	REQUIRE(src != NULL);
-	REQUIRE(dest != NULL && *dest == NULL);
-
-	CHECK(str__new(src->mctx, dest _STR_MEM_FLARG_PASS));
-	CHECK(str_copy(*dest, src));
-
-	return ISC_R_SUCCESS;
-
-cleanup:
-	return result;
-}
-
 void
 str_clear(ld_string_t *dest)
 {
@@ -324,69 +279,6 @@ cleanup:
 	return result;
 }
 
-isc_result_t
-str_cat_char_len(ld_string_t *dest, const char *src, size_t len)
-{
-	isc_result_t result;
-	char *from;
-	size_t dest_size;
-
-	REQUIRE(dest != NULL);
-
-        if (src == NULL || len == 0)
-            return ISC_R_SUCCESS;
-
-	dest_size = str_len_internal(dest);
-
-	CHECK(str_alloc(dest, dest_size + len));
-	from = dest->data + dest_size;
-	memcpy(from, src, len);
-	from[len] = '\0';
-
-	return ISC_R_SUCCESS;
-
-cleanup:
-	return result;
-}
-
-isc_result_t
-str_cat_isc_region(ld_string_t *dest, const isc_region_t *region)
-{
-	REQUIRE(dest != NULL);
-	REQUIRE(region != NULL);
-
-	return str_cat_char_len(dest, (char *)region->base, region->length);
-}
-
-isc_result_t
-str_cat_isc_buffer(ld_string_t *dest, const isc_buffer_t *buffer)
-{
-	isc_region_t region;
-	isc_buffer_t *deconst_buffer;
-
-	REQUIRE(dest != NULL);
-	REQUIRE(ISC_BUFFER_VALID(buffer));
-
-	DE_CONST(buffer, deconst_buffer);
-	isc_buffer_usedregion(deconst_buffer, &region);
-
-	return str_cat_isc_region(dest, &region);
-}
-
-/*
- * Concatenate string src to string dest.
- */
-isc_result_t
-str_cat(ld_string_t *dest, const ld_string_t *src)
-{
-	REQUIRE(dest != NULL);
-
-	if (src == NULL || src->data == NULL)
-            return ISC_R_SUCCESS;
-
-	return str_cat_char(dest, src->data);
-}
-
 /*
  * A sprintf() like function.
  */
@@ -434,40 +326,3 @@ cleanup:
 	va_end(backup);
 	return result;
 }
-
-void
-str_toupper(ld_string_t *str)
-{
-	char *ptr;
-
-	REQUIRE(str != NULL);
-
-	if (str->data == NULL)
-		return;
-
-	for (ptr = str->data; *ptr != '\0'; ptr++)
-		*ptr = toupper((unsigned char)*ptr);
-}
-
-void
-str_to_isc_buffer(const ld_string_t *src, isc_buffer_t *dest)
-{
-	size_t len;
-
-	REQUIRE(src != NULL);
-	REQUIRE(dest != NULL);
-
-	len = str_len_internal(src);
-
-	isc_buffer_init(dest, src->data, len);
-	isc_buffer_add(dest, len);
-}
-
-int
-str_casecmp_char(const ld_string_t *s1, const char *s2)
-{
-	REQUIRE(s1 != NULL && s1->data != NULL);
-	REQUIRE(s2 != NULL);
-
-	return strcasecmp(s1->data, s2);
-}
diff --git a/src/str.h b/src/str.h
index 84056d5100a9f599aa2c424eef4c76cf88902823..7f92ed670d360392dbcbc9f2dc022aadcd42c75b 100644
--- a/src/str.h
+++ b/src/str.h
@@ -48,22 +48,12 @@ typedef struct ld_string	ld_string_t;
 
 size_t str_len(const ld_string_t *str) ATTR_NONNULLS ATTR_CHECKRESULT;
 const char * str_buf(const ld_string_t *src) ATTR_NONNULLS ATTR_CHECKRESULT;
-isc_result_t str_copy(ld_string_t *dest, const ld_string_t *src) ATTR_NONNULLS ATTR_CHECKRESULT;
-isc_result_t str_clone(ld_string_t **dest, const ld_string_t *src _STR_MEM_FLARG) ATTR_NONNULLS ATTR_CHECKRESULT;
 void str_clear(ld_string_t *dest) ATTR_NONNULLS;
 isc_result_t str_init_char(ld_string_t *dest, const char *src) ATTR_NONNULLS ATTR_CHECKRESULT;
 isc_result_t str_cat_char(ld_string_t *dest, const char *src) ATTR_NONNULLS ATTR_CHECKRESULT;
 isc_result_t str_cat_char_len(ld_string_t *dest, const char *src, size_t len) ATTR_NONNULLS ATTR_CHECKRESULT;
-isc_result_t str_cat_isc_region(ld_string_t *dest, const isc_region_t *region) ATTR_NONNULLS ATTR_CHECKRESULT;
-isc_result_t str_cat_isc_buffer(ld_string_t *dest, const isc_buffer_t *buffer) ATTR_NONNULLS ATTR_CHECKRESULT;
-isc_result_t str_cat(ld_string_t *dest, const ld_string_t *src) ATTR_NONNULLS ATTR_CHECKRESULT;
 isc_result_t str_sprintf(ld_string_t *dest, const char *format, ...) ISC_FORMAT_PRINTF(2, 3) ATTR_NONNULLS ATTR_CHECKRESULT;
 isc_result_t str_vsprintf(ld_string_t *dest, const char *format, va_list ap) ATTR_NONNULLS ATTR_CHECKRESULT;
-void str_toupper(ld_string_t *str) ATTR_NONNULLS;
-
-void str_to_isc_buffer(const ld_string_t *src, isc_buffer_t *dest) ATTR_NONNULLS;
-
-int str_casecmp_char(const ld_string_t *s1, const char *s2) ATTR_NONNULLS ATTR_CHECKRESULT;
 
 /* These are pseudo-private functions and shouldn't be called directly. */
 isc_result_t str__new(isc_mem_t *mctx, ld_string_t **new_str _STR_MEM_FLARG) ATTR_NONNULLS ATTR_CHECKRESULT;
-- 
1.9.3

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to