diff extensions/app_radgw/rgw_clients.c @ 516:1c2f5ee38039

Allow RADIUS Proxies with the app_radgw extension
author Sebastien Decugis <sdecugis@nict.go.jp>
date Fri, 27 Aug 2010 10:59:51 +0900
parents d4fc98a3b79c
children 3f43713be92d
line wrap: on
line diff
--- a/extensions/app_radgw/rgw_clients.c	Thu Aug 26 14:10:03 2010 +0900
+++ b/extensions/app_radgw/rgw_clients.c	Fri Aug 27 10:59:51 2010 +0900
@@ -39,6 +39,8 @@
 
 #include "rgw.h"
 
+#define REVERSE_DNS_SIZE_MAX	512 /* length of our buffer for reverse DNS */
+
 /* Ordered lists of clients. The order relationship is a memcmp on the address zone. 
    For same addresses, the port is compared.
    The same address cannot be added twice, once with a 0-port and once with another port value.
@@ -66,6 +68,7 @@
 	
 	/* The FQDN, realm, and optional aliases */
 	int			 is_local; /* true if the RADIUS client runs on the same host -- we use Diameter Identity in that case */
+	enum rgw_cli_type 	 type; /* is it a proxy ? */
 	char 			*fqdn;
 	size_t			 fqdn_len;
 	char			*realm;
@@ -90,7 +93,7 @@
 
 
 /* create a new rgw_client. the arguments are moved into the structure (to limit malloc & free calls). */
-static int client_create(struct rgw_client ** res, struct sockaddr ** ip_port, unsigned char ** key, size_t keylen )
+static int client_create(struct rgw_client ** res, struct sockaddr ** ip_port, unsigned char ** key, size_t keylen, enum rgw_cli_type type )
 {
 	struct rgw_client *tmp = NULL;
 	char buf[255];
@@ -117,6 +120,8 @@
 	memset(tmp, 0, sizeof(struct rgw_client));
 	fd_list_init(&tmp->chain, NULL);
 	
+	tmp->type = type;
+	
 	if (loc) {
 		tmp->is_local = 1;
 	} else {
@@ -230,6 +235,14 @@
 	return 0;
 }
 
+int rgw_clients_gettype(struct rgw_client * cli, enum rgw_cli_type *type)
+{
+	CHECK_PARAMS( cli && type );
+	*type = cli->type;
+	return 0;
+}
+
+
 int rgw_clients_search(struct sockaddr * ip_port, struct rgw_client ** ref)
 {
 	int ret = 0;
@@ -303,15 +316,71 @@
 
 /* Check that the NAS-IP-Adress or NAS-Identifier is coherent with the IP the packet was received from */
 /* Also update the client list of aliases if needed */
-/* NOTE: This function will require changes to allow RADIUS Proxy on the path... */
-int rgw_clients_check_origin(struct rgw_radius_msg_meta *msg, struct rgw_client *cli)
+/* NOTE: This function does nothing if the client is a RADIUS Proxy... */
+/* Check if the message has a valid authenticator, and update the meta-data accordingly */
+int rgw_clients_auth_check(struct rgw_radius_msg_meta * msg, struct rgw_client * cli, uint8_t * req_auth)
+{
+	unsigned char * key;
+	size_t keylen;
+	int count;
+	
+	TRACE_ENTRY("%p %p %p", msg, cli, req_auth);
+	
+	CHECK_PARAMS(msg && cli);
+	
+	CHECK_FCT(rgw_clients_getkey(cli, &key, &keylen));
+	
+	count = radius_msg_count_attr(&msg->radius, RADIUS_ATTR_MESSAGE_AUTHENTICATOR, 0);
+	if (count > 1) {
+		TRACE_DEBUG(INFO, "Too many Message-Authenticator attributes (%d), discarding message.", count);
+		return EINVAL;
+	}
+	if (count == 0) {
+		TRACE_DEBUG(FULL, "Message does not contain a Message-Authenticator attributes.");
+		msg->valid_mac = 0;
+	} else {
+		if (radius_msg_verify_msg_auth( &msg->radius, key, keylen, req_auth )) {
+			TRACE_DEBUG(INFO, "Invalid Message-Authenticator received, discarding message.");
+			return EINVAL;
+		}
+		msg->valid_mac = 1;
+	}
+	
+	return 0;
+}
+
+static struct dict_object * cache_orig_host = NULL;
+static struct dict_object * cache_orig_realm = NULL;
+static struct dict_object * cache_route_record = NULL;
+
+int rgw_clients_init(void)
+{
+	TRACE_ENTRY();
+	CHECK_FCT( fd_dict_search(fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "Origin-Host", &cache_orig_host, ENOENT) );
+	CHECK_FCT( fd_dict_search(fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "Origin-Realm", &cache_orig_realm, ENOENT) );
+	CHECK_FCT( fd_dict_search(fd_g_config->cnf_dict, DICT_AVP, AVP_BY_NAME, "Route-Record", &cache_route_record, ENOENT) );
+	return 0;
+}
+
+
+/* The following function checks if a RADIUS message contains a valid NAS identifier, and initializes an empty Diameter
+ message with the appropriate routing information */
+int rgw_clients_create_origin(struct rgw_radius_msg_meta *msg, struct rgw_client * cli, struct msg ** diam)
 {
 	int idx;
+	int valid_nas_info = 0;
 	struct radius_attr_hdr *nas_ip = NULL, *nas_ip6 = NULL, *nas_id = NULL;
+	char * oh_str = NULL;
+	char * or_str = NULL;
+	char * rr_str = NULL;
+	char buf[REVERSE_DNS_SIZE_MAX]; /* to store DNS lookups results */
 	
-	TRACE_ENTRY("%p %p", msg, cli);
-	CHECK_PARAMS(msg && cli && !msg->valid_nas_info );
-		
+	struct avp *avp = NULL;
+	union avp_value avp_val;
+	
+	TRACE_ENTRY("%p %p %p", msg, cli, diam);
+	CHECK_PARAMS(msg && cli && diam && (*diam == NULL));
+	
 	/* Find the relevant attributes, if any */
 	for (idx = 0; idx < msg->radius.attr_used; idx++) {
 		struct radius_attr_hdr * attr = (struct radius_attr_hdr *)(msg->radius.buf + msg->radius.attr_pos[idx]);
@@ -335,57 +404,111 @@
 		
 	if (!nas_ip && !nas_ip6 && !nas_id) {
 		TRACE_DEBUG(FULL, "The message does not contain any NAS identification attribute.");
-		goto end;
+		
+		/* Get information on this peer */
+		CHECK_FCT( rgw_clients_get_origin(cli, &oh_str, &or_str) );
+		
+		goto diameter;
 	}
 	
 	/* Check if the message was received from the IP in NAS-IP-Address attribute */
 	if (nas_ip && (cli->sa->sa_family == AF_INET) && !memcmp(nas_ip+1, &cli->sin->sin_addr, sizeof(struct in_addr))) {
 		TRACE_DEBUG(FULL, "NAS-IP-Address contains the same address as the message was received from.");
-		msg->valid_nas_info |= 1;
+		valid_nas_info |= 1;
 	}
 	if (nas_ip6 && (cli->sa->sa_family == AF_INET6) && !memcmp(nas_ip6+1, &cli->sin6->sin6_addr, sizeof(struct in6_addr))) {
 		TRACE_DEBUG(FULL, "NAS-IPv6-Address contains the same address as the message was received from.");
-		msg->valid_nas_info |= 1;
+		valid_nas_info |= 2;
 	}
 	
-	/* If these conditions are not met, the message is probably forged (well, this might be false...) */
-	if ((! msg->valid_nas_info) && (nas_ip || nas_ip6)) {
-		/*
-				In RADIUS it would be possible for a rogue NAS to forge the NAS-IP-
-				Address attribute value.  Diameter/RADIUS translation agents MUST
-				check a received NAS-IP-Address or NAS-IPv6-Address attribute against
-				the source address of the RADIUS packet.  If they do not match and
-				the Diameter/RADIUS translation agent does not know whether the
-				packet was sent by a RADIUS proxy or NAS (e.g., no Proxy-State
-				attribute), then by default it is assumed that the source address
-				corresponds to a RADIUS proxy, and that the NAS Address is behind
-				that proxy, potentially with some additional RADIUS proxies in
-				between.  The Diameter/RADIUS translation agent MUST insert entries
-				in the Route-Record AVP corresponding to the apparent route.  This
-				implies doing a reverse lookup on the source address and NAS-IP-
-				Address or NAS-IPv6-Address attributes to determine the corresponding
-				FQDNs.
+	
+	/*
+			In RADIUS it would be possible for a rogue NAS to forge the NAS-IP-
+			Address attribute value.  Diameter/RADIUS translation agents MUST
+			check a received NAS-IP-Address or NAS-IPv6-Address attribute against
+			the source address of the RADIUS packet.  If they do not match and
+			the Diameter/RADIUS translation agent does not know whether the
+			packet was sent by a RADIUS proxy or NAS (e.g., no Proxy-State
+			attribute), then by default it is assumed that the source address
+			corresponds to a RADIUS proxy, and that the NAS Address is behind
+			that proxy, potentially with some additional RADIUS proxies in
+			between.  The Diameter/RADIUS translation agent MUST insert entries
+			in the Route-Record AVP corresponding to the apparent route.  This
+			implies doing a reverse lookup on the source address and NAS-IP-
+			Address or NAS-IPv6-Address attributes to determine the corresponding
+			FQDNs.
+
+			If the source address and the NAS-IP-Address or NAS-IPv6-Address do
+			not match, and the Diameter/RADIUS translation agent knows that it is
+			talking directly to the NAS (e.g., there are no RADIUS proxies
+			between it and the NAS), then the error should be logged, and the
+			packet MUST be discarded.
 
-				If the source address and the NAS-IP-Address or NAS-IPv6-Address do
-				not match, and the Diameter/RADIUS translation agent knows that it is
-				talking directly to the NAS (e.g., there are no RADIUS proxies
-				between it and the NAS), then the error should be logged, and the
-				packet MUST be discarded.
-
-				Diameter agents and servers MUST check whether the NAS-IP-Address AVP
-				corresponds to an entry in the Route-Record AVP.  This is done by
-				doing a reverse lookup (PTR RR) for the NAS-IP-Address to retrieve
-				the corresponding FQDN, and by checking for a match with the Route-
-				Record AVP.  If no match is found, then an error is logged, but no
-				other action is taken.
-		*/
-		TRACE_DEBUG(INFO, "Message received with a NAS-IP-Address or NAS-IPv6-Address different from the sender's. Discarding...");
-		return ENOTSUP;
+			Diameter agents and servers MUST check whether the NAS-IP-Address AVP
+			corresponds to an entry in the Route-Record AVP.  This is done by
+			doing a reverse lookup (PTR RR) for the NAS-IP-Address to retrieve
+			the corresponding FQDN, and by checking for a match with the Route-
+			Record AVP.  If no match is found, then an error is logged, but no
+			other action is taken.
+	*/
+	if (nas_ip || nas_ip6) {
+		if (!valid_nas_info) {
+			if (cli->type == RGW_CLI_NAS) {
+				TRACE_DEBUG(INFO, "Message received with a NAS-IP-Address or NAS-IPv6-Address different \nfrom the sender's. Please configure as Proxy if this is expected.\n Message discarded.");
+				return EINVAL;
+			} else {
+				/* the peer is configured as a proxy, so accept the message */
+				sSS ss;
+				
+				/* In that case, the cli will be stored as Route-Record and the NAS-IP-Address as origin */
+				if (!cli->is_local) {
+					rr_str = cli->fqdn;
+				}
+				
+				/* We must DNS-reverse the NAS-IP*-Address */
+				memset(&ss, 0 , sizeof(sSS));
+				if (nas_ip) {
+					sSA4 * sin = (sSA4 *)&ss;
+					sin->sin_family = AF_INET;
+					memcpy(&sin->sin_addr, nas_ip + 1, sizeof(struct in_addr));
+				} else {
+					sSA6 * sin6 = (sSA6 *)&ss;
+					sin6->sin6_family = AF_INET6;
+					memcpy(&sin6->sin6_addr, nas_ip6 + 1, sizeof(struct in6_addr));
+				}
+				CHECK_SYS_DO( getnameinfo( (sSA *)&ss, sSAlen(&ss), &buf[0], sizeof(buf), NULL, 0, NI_NAMEREQD),
+					{
+						TRACE_DEBUG(INFO, "The NAS-IP*-Address cannot be DNS reversed in order to create the Origin-Host AVP; rejecting the message (translation is impossible).");
+						return EINVAL;
+					} );
+				
+				oh_str = &buf[0];
+				or_str = strchr(oh_str, '.');
+				if (or_str) {
+					or_str ++; /* move after the first dot */
+					if (*or_str == '\0')
+						or_str = NULL; /* Discard this realm, we will use the local realm later */
+				}
+			}
+		} else {
+			/* The attribute matches the source address, just use this in origin-host */
+			CHECK_FCT( rgw_clients_get_origin(cli, &oh_str, &or_str) );
+		}
+		
+		goto diameter; /* we ignore the nas_id in that case */
 	}
 	
-	/* Now check the nas_id, but only for non-local hosts */
-	if (nas_id && (! cli->is_local) ) {
-		char * str;
+	/* We don't have a NAS-IP*-Address attribute if we are here */
+	if (cli->is_local) {
+		/* Simple: we use our own configuration */
+		CHECK_FCT( rgw_clients_get_origin(cli, &oh_str, &or_str) );
+		goto diameter;
+	}
+	
+	/* At this point, we only have nas_id, and the client is not local */
+	ASSERT(nas_id);
+	
+	{
 		int found, ret;
 		struct addrinfo hint, *res, *ptr;
 		
@@ -409,7 +532,7 @@
 		/* first, check if the nas_id is the fqdn of the peer or a known alias */
 		if ((cli->fqdn_len == (nas_id->length - sizeof(struct radius_attr_hdr))) 
 		&& (!strncasecmp((char *)(nas_id + 1), cli->fqdn, nas_id->length - sizeof(struct radius_attr_hdr)))) {
-			TRACE_DEBUG(FULL, "NAS-Identifier contains the fqdn of the NAS");
+			TRACE_DEBUG(FULL, "NAS-Identifier contains the fqdn of the client");
 			found = 1;
 		} else {
 			for (idx = 0; idx < cli->aliases_nb; idx++) {
@@ -423,52 +546,106 @@
 		}
 		
 		if (found) {
-			msg->valid_nas_info |= 2;
-			goto end;
+			/* The NAS-Identifier matches the source IP */
+			CHECK_FCT( rgw_clients_get_origin(cli, &oh_str, &or_str) );
+
+			goto diameter;
 		}
 		
-		/* copy the identifier, we try to DNS resolve it */
-		CHECK_MALLOC( str = malloc(nas_id->length - sizeof(struct radius_attr_hdr) + 1) );
-		memcpy(str, nas_id + 1, nas_id->length - sizeof(struct radius_attr_hdr));
-		str[nas_id->length - sizeof(struct radius_attr_hdr)] = '\0';
+		/* Attempt DNS resolution of the identifier */
+		ASSERT( nas_id->length - sizeof(struct radius_attr_hdr) < sizeof(buf) );
+		memcpy(buf, nas_id + 1, nas_id->length - sizeof(struct radius_attr_hdr));
+		buf[nas_id->length - sizeof(struct radius_attr_hdr)] = '\0';
 		
 		/* Now check if this alias is valid for this peer */
 		memset(&hint, 0, sizeof(hint));
-		hint.ai_family = cli->sa->sa_family;
 		hint.ai_flags  = AI_CANONNAME;
-		ret = getaddrinfo(str, NULL, &hint, &res);
+		ret = getaddrinfo(buf, NULL, &hint, &res);
 		if (ret == 0) {
-			/* The name was resolved correctly, it must match the IP of the client: */
+			strncpy(buf, res->ai_canonname, sizeof(buf));
+			/* The name was resolved correctly, does it match the IP of the client? */
 			for (ptr = res; ptr != NULL; ptr = ptr->ai_next) {
 				if (cli->sa->sa_family != ptr->ai_family)
 					continue;
 				if (memcmp(cli->sa, ptr->ai_addr, sSAlen(cli->sa)))
 					continue;
 				
-				/* It matches: the alias is valid */
 				found = 1;
 				break;
 			}
 			freeaddrinfo(res);
 			
 			if (!found) {
-				TRACE_DEBUG(INFO, "The NAS-Identifier value '%s' resolves to a different IP from the NAS's, discarding the message.", str);
-				free(str);
-				return EINVAL;
+				if (cli->type == RGW_CLI_NAS) {
+					TRACE_DEBUG(INFO, "The NAS-Identifier value '%.*s' resolves to a different IP than the client's, discarding the message. \nConfigure this client as a Proxy if this message should be valid.", 
+						nas_id->length - sizeof(struct radius_attr_hdr), nas_id + 1);
+					return EINVAL;
+				} else {
+					/* This identifier matches a different IP, assume it is a proxied message */
+					if (!cli->is_local) {
+						rr_str = cli->fqdn;
+					}
+					oh_str = &buf[0]; /* The canonname resolved */
+					or_str = strchr(oh_str, '.');
+					if (or_str) {
+						or_str ++; /* move after the first dot */
+						if (*or_str == '\0')
+							or_str = NULL; /* Discard this realm, we will use the local realm later */
+					}
+				}
+			} else {
+				/* It is a valid alias, save it */
+				CHECK_MALLOC( cli->aliases = realloc(cli->aliases, (cli->aliases_nb + 1) * sizeof(char *)) );
+				CHECK_MALLOC( cli->aliases[cli->aliases_nb + 1] = malloc( 1 + nas_id->length - sizeof(struct radius_attr_hdr) ));
+				memcpy( cli->aliases[cli->aliases_nb + 1], nas_id + 1, nas_id->length - sizeof(struct radius_attr_hdr));
+				*(cli->aliases[cli->aliases_nb + 1] + nas_id->length - sizeof(struct radius_attr_hdr)) = '\0';
+				cli->aliases_nb ++;
+				TRACE_DEBUG(FULL, "Saved valid alias for client: '%s' -> '%s'", cli->aliases[cli->aliases_nb + 1], cli->fqdn);
+				CHECK_FCT( rgw_clients_get_origin(cli, &oh_str, &or_str) );
 			}
 		} else {
 			/* Error resolving the name */
-			TRACE_DEBUG(INFO, "Error while resolving NAS-Identifier value '%s': %s. Ignoring...", str, gai_strerror(ret));
+			TRACE_DEBUG(INFO, "NAS-Identifier '%s' cannot be resolved: %s. Ignoring...", buf, gai_strerror(ret));
+			/* Assume this is a valid identifier for the client */
+			CHECK_FCT( rgw_clients_get_origin(cli, &oh_str, &or_str) );
 		}
-		
-		/* It is a valid alias, save it */
-		CHECK_MALLOC( cli->aliases = realloc(cli->aliases, (cli->aliases_nb + 1) * sizeof(char *)) );
-		cli->aliases[cli->aliases_nb + 1] = str;
-		cli->aliases_nb ++;
-		TRACE_DEBUG(FULL, "Saved valid alias for client: '%s' -> '%s'", str, cli->fqdn);
-		msg->valid_nas_info |= 2;
 	}
-end:	
+	
+	/* Now, let's create the empty Diameter message with Origin-Host, -Realm, and Route-Record if needed. */
+diameter:
+	ASSERT(oh_str); /* If it is not defined here, there is a bug... */
+	if (!or_str)
+		or_str = fd_g_config->cnf_diamrlm; /* Use local realm in that case */
+	
+	/* Create an empty Diameter message so that extensions can store their AVPs */
+	CHECK_FCT(  fd_msg_new ( NULL, MSGFL_ALLOC_ETEID, diam )  );
+	
+	/* Add the Origin-Host as next AVP */
+	CHECK_FCT( fd_msg_avp_new ( cache_orig_host, 0, &avp ) );
+	memset(&avp_val, 0, sizeof(avp_val));
+	avp_val.os.data = (unsigned char *)oh_str;
+	avp_val.os.len = strlen(oh_str);
+	CHECK_FCT( fd_msg_avp_setvalue ( avp, &avp_val ) );
+	CHECK_FCT( fd_msg_avp_add ( *diam, MSG_BRW_LAST_CHILD, avp) );
+	
+	/* Add the Origin-Realm as next AVP */
+	CHECK_FCT( fd_msg_avp_new ( cache_orig_realm, 0, &avp ) );
+	memset(&avp_val, 0, sizeof(avp_val));
+	avp_val.os.data = (unsigned char *)or_str;
+	avp_val.os.len = strlen(or_str);
+	CHECK_FCT( fd_msg_avp_setvalue ( avp, &avp_val ) );
+	CHECK_FCT( fd_msg_avp_add ( *diam, MSG_BRW_LAST_CHILD, avp) );
+	
+	if (rr_str) {
+		CHECK_FCT( fd_msg_avp_new ( cache_route_record, 0, &avp ) );
+		memset(&avp_val, 0, sizeof(avp_val));
+		avp_val.os.data = (unsigned char *)rr_str;
+		avp_val.os.len = strlen(rr_str);
+		CHECK_FCT( fd_msg_avp_setvalue ( avp, &avp_val ) );
+		CHECK_FCT( fd_msg_avp_add ( *diam, MSG_BRW_LAST_CHILD, avp) );
+	}
+	
+	/* Done! */
 	return 0;
 }
 
@@ -507,7 +684,7 @@
 	CHECK_POSIX_DO( pthread_mutex_unlock(&cli_mtx), );
 }
 
-int rgw_clients_add( struct sockaddr * ip_port, unsigned char ** key, size_t keylen )
+int rgw_clients_add( struct sockaddr * ip_port, unsigned char ** key, size_t keylen, enum rgw_cli_type type )
 {
 	struct rgw_client * prev = NULL, *new = NULL;
 	int ret;
@@ -516,10 +693,11 @@
 	
 	CHECK_PARAMS( ip_port && key && *key && keylen );
 	CHECK_PARAMS( (ip_port->sa_family == AF_INET) || (ip_port->sa_family == AF_INET6) );
+	CHECK_PARAMS( (type == RGW_CLI_NAS) || (type == RGW_CLI_PXY) );
 	
 	/* Dump the entry in debug mode */
 	if (TRACE_BOOL(FULL + 1 )) {
-		TRACE_DEBUG(FULL, "Adding client:");
+		TRACE_DEBUG(FULL, "Adding %s:", (type == RGW_CLI_NAS) ? "NAS" : "PROXY"  );
 		TRACE_DEBUG_sSA(FULL, 	 "\tIP : ", ip_port, NI_NUMERICHOST | NI_NUMERICSERV, "" );
 		TRACE_DEBUG_BUFFER(FULL, "\tKey: [", *key, keylen, "]" );
 	}
@@ -531,7 +709,7 @@
 	ret = client_search(&prev, ip_port );
 	if (ret == ENOENT) {
 		/* No duplicate found, Ok to add */
-		CHECK_FCT_DO( ret = client_create( &new, &ip_port, key, keylen ), goto end );
+		CHECK_FCT_DO( ret = client_create( &new, &ip_port, key, keylen, type ), goto end );
 		fd_list_insert_after(&prev->chain, &new->chain);
 		new->refcount++;
 		ret = 0;
@@ -540,17 +718,17 @@
 	
 	if (ret == EEXIST) {
 		/* Check if the key is the same, then skip or return an error */
-		if ((keylen == prev->key.len ) && ( ! memcmp(*key, prev->key.data, keylen) )) {
+		if ((keylen == prev->key.len ) && ( ! memcmp(*key, prev->key.data, keylen) ) && (type == prev->type)) {
 			TRACE_DEBUG(INFO, "Skipping duplicate client description");
 			ret = 0;
 			goto end;
 		}
 		
 		fd_log_debug("ERROR: Conflicting RADIUS clients descriptions!\n");
-		TRACE_DEBUG(NONE, "Previous entry:");
+		TRACE_DEBUG(NONE, "Previous entry: %s", (prev->type == RGW_CLI_NAS) ? "NAS" : "PROXY");
 		TRACE_DEBUG_sSA(NONE, 	 "\tIP : ", prev->sa, NI_NUMERICHOST | NI_NUMERICSERV, "" );
 		TRACE_DEBUG_BUFFER(NONE, "\tKey: [", prev->key.data, prev->key.len, "]" );
-		TRACE_DEBUG(NONE, "Conflicting entry:");
+		TRACE_DEBUG(NONE, "Conflicting entry: %s", (type == RGW_CLI_NAS) ? "NAS" : "PROXY");
 		TRACE_DEBUG_sSA(NONE, 	 "\tIP : ", ip_port, NI_NUMERICHOST | NI_NUMERICSERV, "" );
 		TRACE_DEBUG_BUFFER(NONE, "\tKey: [", *key, keylen, "]" );
 	}
"Welcome to our mercurial repository"