From c93b496aedb6dce6e91ffc61436a797f427d3309 Mon Sep 17 00:00:00 2001
From: Florian Westphal <fw@strlen.de>
Date: Mon, 18 Feb 2008 15:24:01 +0100
Subject: [PATCH] Add support for up to 3 targets in WHOIS queries.

also allow up to one wildcard query from local hosts.
Follows ircd 2.10 implementation rather than RFC 2812.
At most 10 entries are returned per wildcard expansion.

WHOIS test cases by Dana Dahlstrom.
---
 src/ngircd/client.c       |    2 +-
 src/ngircd/client.h       |    2 +-
 src/ngircd/irc-info.c     |  187 ++++++++++++++++++++++++++++++---------------
 src/testsuite/Makefile.am |   11 ++-
 4 files changed, 134 insertions(+), 68 deletions(-)

diff --git a/src/ngircd/client.c b/src/ngircd/client.c
index c7d6427..8c1b7a9 100644
--- a/src/ngircd/client.c
+++ b/src/ngircd/client.c
@@ -532,7 +532,7 @@ Client_ModeDel( CLIENT *Client, char Mode )
 
 
 GLOBAL CLIENT *
-Client_Search( char *Nick )
+Client_Search( const char *Nick )
 {
 	/* return Client-Structure that has the corresponding Nick.
 	 * If none is found, return NULL.
diff --git a/src/ngircd/client.h b/src/ngircd/client.h
index d211d23..63406f2 100644
--- a/src/ngircd/client.h
+++ b/src/ngircd/client.h
@@ -85,7 +85,7 @@ GLOBAL CLIENT *Client_ThisServer PARAMS(( void ));
 
 GLOBAL CLIENT *Client_GetFromToken PARAMS(( CLIENT *Client, int Token ));
 
-GLOBAL CLIENT *Client_Search PARAMS(( char *ID ));
+GLOBAL CLIENT *Client_Search PARAMS(( const char *ID ));
 GLOBAL CLIENT *Client_First PARAMS(( void ));
 GLOBAL CLIENT *Client_Next PARAMS(( CLIENT *c ));
 
diff --git a/src/ngircd/irc-info.c b/src/ngircd/irc-info.c
index 87ed2ad..5ec22cb 100644
--- a/src/ngircd/irc-info.c
+++ b/src/ngircd/irc-info.c
@@ -885,90 +885,65 @@ IRC_WHO( CLIENT *Client, REQUEST *Req )
 } /* IRC_WHO */
 
 
-GLOBAL bool
-IRC_WHOIS( CLIENT *Client, REQUEST *Req )
+static bool
+IRC_WHOIS_SendReply(CLIENT *Client, CLIENT *from, CLIENT *c)
 {
-	CLIENT *from, *target, *c;
 	char str[LINE_LEN + 1];
 	CL2CHAN *cl2chan;
 	CHANNEL *chan;
 
-	assert( Client != NULL );
-	assert( Req != NULL );
-
-	/* Bad number of parameters? */
-	if(( Req->argc < 1 ) || ( Req->argc > 2 )) return IRC_WriteStrClient( Client, ERR_NEEDMOREPARAMS_MSG, Client_ID( Client ), Req->command );
-
-	/* Search client */
-	c = Client_Search( Req->argv[Req->argc - 1] );
-	if(( ! c ) || ( Client_Type( c ) != CLIENT_USER )) return IRC_WriteStrClient( Client, ERR_NOSUCHNICK_MSG, Client_ID( Client ), Req->argv[Req->argc - 1] );
-
-	/* Search sender of the WHOIS */
-	if( Client_Type( Client ) == CLIENT_SERVER ) from = Client_Search( Req->prefix );
-	else from = Client;
-	if( ! from ) return IRC_WriteStrClient( Client, ERR_NOSUCHNICK_MSG, Client_ID( Client ), Req->prefix );
-
-	/* Forward to other server? */
-	if( Req->argc > 1 )
-	{
-		/* Search target server (can be specified as nick of that server!) */
-		target = Client_Search( Req->argv[0] );
-		if( ! target ) return IRC_WriteStrClient( from, ERR_NOSUCHSERVER_MSG, Client_ID( from ), Req->argv[0] );
-	}
-	else target = Client_ThisServer( );
-
-	assert( target != NULL );
-
-	if(( Client_NextHop( target ) != Client_ThisServer( )) && ( Client_Type( Client_NextHop( target )) == CLIENT_SERVER )) return IRC_WriteStrClientPrefix( target, from, "WHOIS %s :%s", Req->argv[0], Req->argv[1] );
-
 	/* Nick, user and name */
-	if( ! IRC_WriteStrClient( from, RPL_WHOISUSER_MSG, Client_ID( from ), Client_ID( c ), Client_User( c ), Client_Hostname( c ), Client_Info( c ))) return DISCONNECTED;
+	if (!IRC_WriteStrClient(from, RPL_WHOISUSER_MSG, Client_ID(from),
+		Client_ID(c), Client_User(c), Client_Hostname(c), Client_Info(c)))
+			return DISCONNECTED;
 
 	/* Server */
-	if( ! IRC_WriteStrClient( from, RPL_WHOISSERVER_MSG, Client_ID( from ), Client_ID( c ), Client_ID( Client_Introducer( c )), Client_Info( Client_Introducer( c )))) return DISCONNECTED;
+	if (!IRC_WriteStrClient(from, RPL_WHOISSERVER_MSG, Client_ID(from),
+		Client_ID(c), Client_ID(Client_Introducer(c)), Client_Info(Client_Introducer(c))))
+			return DISCONNECTED;
 
 	/* Channels */
-	snprintf( str, sizeof( str ), RPL_WHOISCHANNELS_MSG, Client_ID( from ), Client_ID( c ));
-	cl2chan = Channel_FirstChannelOf( c );
-	while( cl2chan )
-	{
-		chan = Channel_GetChannel( cl2chan );
-		assert( chan != NULL );
+	snprintf(str, sizeof(str), RPL_WHOISCHANNELS_MSG, Client_ID(from), Client_ID(c));
+	cl2chan = Channel_FirstChannelOf(c);
+	while (cl2chan) {
+		chan = Channel_GetChannel(cl2chan);
+		assert(chan != NULL);
 
 		/* next */
-		cl2chan = Channel_NextChannelOf( c, cl2chan );
+		cl2chan = Channel_NextChannelOf(c, cl2chan);
 
 		/* Secret channel? */
-		if( strchr( Channel_Modes( chan ), 's' ) && ! Channel_IsMemberOf( chan, Client )) continue;
+		if (strchr(Channel_Modes(chan), 's') && !Channel_IsMemberOf(chan, Client))
+			continue;
 
 		/* Concatenate channel names */
-		if( str[strlen( str ) - 1] != ':' ) strlcat( str, " ", sizeof( str ));
-		if( strchr( Channel_UserModes( chan, c ), 'o' )) strlcat( str, "@", sizeof( str ));
-		else if( strchr( Channel_UserModes( chan, c ), 'v' )) strlcat( str, "+", sizeof( str ));
-		strlcat( str, Channel_Name( chan ), sizeof( str ));
+		if (str[strlen(str) - 1] != ':')
+			strlcat(str, " ", sizeof(str));
 
-		if( strlen( str ) > ( LINE_LEN - CHANNEL_NAME_LEN - 4 ))
-		{
+		strlcat(str, who_flags_qualifier(Channel_UserModes(chan, c)), sizeof(str));
+		strlcat(str, Channel_Name(chan), sizeof(str));
+
+		if (strlen(str) > (LINE_LEN - CHANNEL_NAME_LEN - 4)) {
 			/* Line becomes too long: send it! */
-			if( ! IRC_WriteStrClient( Client, "%s", str )) return DISCONNECTED;
-			snprintf( str, sizeof( str ), RPL_WHOISCHANNELS_MSG, Client_ID( from ), Client_ID( c ));
+			if (!IRC_WriteStrClient(Client, "%s", str))
+				return DISCONNECTED;
+			snprintf(str, sizeof(str), RPL_WHOISCHANNELS_MSG, Client_ID(from), Client_ID(c));
 		}
 	}
-	if( str[strlen( str ) - 1] != ':')
-	{
+	if(str[strlen(str) - 1] != ':') {
 		/* There is data left to send: */
-		if( ! IRC_WriteStrClient( Client, "%s", str )) return DISCONNECTED;
+		if (!IRC_WriteStrClient(Client, "%s", str))
+			return DISCONNECTED;
 	}
 
 	/* IRC-Operator? */
-	if( Client_HasMode( c, 'o' ))
-	{
-		if( ! IRC_WriteStrClient( from, RPL_WHOISOPERATOR_MSG, Client_ID( from ), Client_ID( c ))) return DISCONNECTED;
-	}
+	if (Client_HasMode(c, 'o') &&
+		!IRC_WriteStrClient(from, RPL_WHOISOPERATOR_MSG, Client_ID(from), Client_ID(c)))
+			return DISCONNECTED;
 
 	/* Idle and signon time (local clients only!) */
-	if (Client_Conn(c) > NONE ) {
-		if (! IRC_WriteStrClient(from, RPL_WHOISIDLE_MSG,
+	if (Client_Conn(c) > NONE) {
+		if (!IRC_WriteStrClient(from, RPL_WHOISIDLE_MSG,
 			Client_ID(from), Client_ID(c),
 			(unsigned long)Conn_GetIdle(Client_Conn(c)),
 			(unsigned long)Conn_GetSignon(Client_Conn(c))))
@@ -976,13 +951,99 @@ IRC_WHOIS( CLIENT *Client, REQUEST *Req )
 	}
 
 	/* Away? */
-	if( Client_HasMode( c, 'a' ))
-	{
-		if( ! IRC_WriteStrClient( from, RPL_AWAY_MSG, Client_ID( from ), Client_ID( c ), Client_Away( c ))) return DISCONNECTED;
+	if (Client_HasMode(c, 'a') &&
+		!IRC_WriteStrClient(from, RPL_AWAY_MSG, Client_ID(from), Client_ID(c), Client_Away(c)))
+			return DISCONNECTED;
+	return IRC_WriteStrClient(from, RPL_ENDOFWHOIS_MSG, Client_ID(from), Client_ID(c));
+}
+
+
+GLOBAL bool
+IRC_WHOIS( CLIENT *Client, REQUEST *Req )
+{
+	CLIENT *from, *target, *c;
+	unsigned int match_count = 0, found = 0;
+	bool has_wildcards, is_remote;
+	bool got_wildcard = false;
+	const char *query;
+
+	assert( Client != NULL );
+	assert( Req != NULL );
+
+	if ((Req->argc < 1) || (Req->argc > 2))
+		return IRC_WriteStrClient(Client, ERR_NEEDMOREPARAMS_MSG, Client_ID(Client), Req->command);
+
+	/* Search sender of the WHOIS */
+	if (Client_Type(Client) == CLIENT_SERVER) {
+		from = Client_Search(Req->prefix);
+	} else {
+		IRC_SetPenalty(Client, 1);
+		from = Client;
 	}
+	if (!from)
+		return IRC_WriteStrClient(Client, ERR_NOSUCHNICK_MSG, Client_ID(Client), Req->prefix);
 
-	/* End of Whois */
-	return IRC_WriteStrClient( from, RPL_ENDOFWHOIS_MSG, Client_ID( from ), Client_ID( c ));
+	/* Forward to other server? */
+	if (Req->argc > 1) {
+		/* Search target server (can be specified as nick of that server!) */
+		target = Client_Search(Req->argv[0]);
+		if (!target)
+			return IRC_WriteStrClient(from, ERR_NOSUCHSERVER_MSG, Client_ID(from), Req->argv[0]);
+	} else {
+		target = Client_ThisServer();
+		assert(target != NULL);
+	}
+
+	if ((Client_NextHop(target) != Client_ThisServer()) &&
+		(Client_Type(Client_NextHop(target)) == CLIENT_SERVER))
+			return IRC_WriteStrClientPrefix(target, from, "WHOIS %s :%s", Req->argv[0], Req->argv[1]);
+
+	is_remote = Client_Conn(from) < 0;
+	for (query = strtok(Req->argv[Req->argc - 1], ",");
+			query && found < 3;
+			query = strtok(NULL, ","), found++)
+	{
+		has_wildcards = query[strcspn(query, "*?")] != 0;
+		/*
+		 * follows ircd 2.10 implementation:
+		 *  - handle up to 3 targets
+		 *  - no wildcards for remote clients
+		 *  - only one wildcard target per local client
+		 *
+		 *  also, at most ten matches are returned.
+		 */
+		if (!has_wildcards || is_remote) {
+			c = Client_Search(query);
+			if (c) {
+				if (!IRC_WHOIS_SendReply(Client, from, c))
+					return DISCONNECTED;
+			} else {
+				if (!IRC_WriteStrClient(Client, ERR_NOSUCHNICK_MSG, Client_ID(Client), query))
+					return DISCONNECTED;
+			}
+			continue;
+		}
+		if (got_wildcard) { /* we already handled one wildcard query */
+			if (!IRC_WriteStrClient(Client, ERR_NOSUCHNICK_MSG, Client_ID(Client), query))
+				return DISCONNECTED;
+			continue;
+		}
+		got_wildcard = true;
+		IRC_SetPenalty(Client, 3);
+		for (c = Client_First(); c && match_count < 10; c = Client_Next(c)) {
+			if (Client_Type(c) != CLIENT_USER)
+				continue;
+			if (!MatchCaseInsensitive(query, Client_ID(c)))
+				continue;
+			if (!IRC_WHOIS_SendReply(Client, from, c))
+				return DISCONNECTED;
+			match_count++;
+		}
+
+		if (match_count == 0)
+			return IRC_WriteStrClient(Client, ERR_NOSUCHNICK_MSG, Client_ID(Client), Req->argv[Req->argc - 1]);
+	}
+	return CONNECTED;
 } /* IRC_WHOIS */
 
 
diff --git a/src/testsuite/Makefile.am b/src/testsuite/Makefile.am
index 6512aaa..59096dc 100644
--- a/src/testsuite/Makefile.am
+++ b/src/testsuite/Makefile.am
@@ -47,13 +47,17 @@ channel-test: tests.sh
 	rm -f channel-test
 	ln -s $(srcdir)/tests.sh channel-test
 
+misc-test: tests.sh
+	rm -f misc-test
+	ln -s $(srcdir)/tests.sh misc-test
+
 who-test: tests.sh
 	rm -f who-test
 	ln -s $(srcdir)/tests.sh who-test
 
-misc-test: tests.sh
-	rm -f misc-test
-	ln -s $(srcdir)/tests.sh misc-test
+whois-test: tests.sh
+	rm -f whois-test
+	ln -s $(srcdir)/tests.sh whois-test
 
 mode-test: tests.sh
 	rm -f mode-test
@@ -65,6 +69,7 @@ TESTS = start-server.sh \
 	misc-test \
 	mode-test \
 	who-test \
+	whois-test \
 	stress-server.sh \
 	stop-server.sh
 
-- 
1.5.3.7

