/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.protocol.irc; import java.util.regex.*; import net.java.sip.communicator.util.*; import com.ircclouds.irc.api.*; import com.ircclouds.irc.api.domain.messages.*; import com.ircclouds.irc.api.state.*; /** * Identity manager. * * TODO Add support for Identity Service (NickServ) instance that can be used * for accessing remote identity facilities. * * TODO Implement OperationSetChangePassword once an identity service is * available. * * TODO Query remote identity service for current identity-state such as: * unknown, unauthenticated, authenticated. * * @author Danny van Heumen */ public class IdentityManager { /** * Logger. */ private static final Logger LOGGER = Logger .getLogger(IdentityManager.class); /** * Pattern of a valid nick. */ public static final Pattern NICK_PATTERN = Pattern .compile("[A-Za-z\\[\\]\\\\`\\^\\{\\}_\\|]" + "[A-Za-z0-9\\-\\[\\]\\\\`\\^\\{\\}_\\|]*"); /** * The IRCApi instance. * * Instance must be thread-safe! */ private final IRCApi irc; /** * The connection state. */ private final IIRCState connectionState; /** * The protocol provider instance. */ private final ProtocolProviderServiceIrcImpl provider; /** * The identity container. */ private final Identity identity = new Identity(); /** * Maximum nick length according to server ISUPPORT instructions. * *

This value is not guaranteed, so it may be null.

*/ private final Integer isupportNickLen; /** * Constructor. * * @param irc thread-safe IRCApi instance * @param connectionState the connection state * @param provider the protocol provider instance */ public IdentityManager(final IRCApi irc, final IIRCState connectionState, final ProtocolProviderServiceIrcImpl provider) { if (irc == null) { throw new IllegalArgumentException("irc instance cannot be null"); } this.irc = irc; if (connectionState == null) { throw new IllegalArgumentException( "connectionState instance cannot be null"); } this.connectionState = connectionState; if (provider == null) { throw new IllegalArgumentException("provider cannot be null"); } this.provider = provider; this.irc.addListener(new IdentityListener()); // query user's WHOIS identity as perceived by the IRC server queryIdentity(this.irc, this.connectionState, new WhoisListener()); isupportNickLen = parseISupportNickLen(this.connectionState); } /** * Issue WHOIS query to discover identity as seen by the server. */ private static void queryIdentity(final IRCApi irc, final IIRCState state, final WhoisListener listener) { // This method should be as light-weight as possible, since it is called // from the constructor. new Thread() { public void run() { try { irc.addListener(listener); irc.rawMessage("WHOIS " + state.getNickname()); } catch (final RuntimeException e) { LOGGER.error("Failed to deliver WHOIS message.", e); } }; }.start(); } /** * Parse the ISUPPORT parameter for server's max nick length. * * @param state the connection state * @return returns instance with max nick length or null if not * specified. */ private static Integer parseISupportNickLen(final IIRCState state) { final String value = state.getServerOptions().getKey(ISupport.NICKLEN.name()); if (value == null) { return null; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Setting ISUPPORT parameter " + ISupport.NICKLEN.name() + " to " + value); } return new Integer(value); } /** * Get the nick name of the user. * * @return Returns either the acting nick if a connection is established or * the configured nick. */ public String getNick() { return this.connectionState.getNickname(); } /** * Set a new nick name. * * @param nick new nick */ public void setNick(final String nick) { this.irc.changeNick(checkNick(nick, this.isupportNickLen)); } /** * Verify nick name. * * @param nick nick name * @param isupportNickLen maximum nick length according to server * parameters. * @return returns nick name */ public static String checkNick(final String nick, final Integer isupportNickLen) { if (nick == null || nick.isEmpty()) { throw new IllegalArgumentException("a nick name must be provided"); } if (!NICK_PATTERN.matcher(nick).matches()) { throw new IllegalArgumentException( "nick name contains invalid characters: only letters, " + "digits and -, \\, [, ], `, ^, {, }, |, _ are allowed"); } if (isupportNickLen != null && nick.length() > isupportNickLen) { throw new IllegalArgumentException("the nick name must not be " + "longer than " + isupportNickLen.intValue() + " characters " + "according to server parameters."); } return nick; } /** * Get the current identity string, based on nick, user and host of local * user. * * @return returns identity string */ public String getIdentityString() { final String currentNick = this.connectionState.getNickname(); return this.identity.getString(currentNick); } /** * The Whois listener which uses the WHOIS data for the local user to update * the identity information in the provided container. * * @author Danny van Heumen */ private final class WhoisListener extends AbstractIrcMessageListener { /** * IRC reply for WHOIS query. */ private static final int RPL_WHOISUSER = 311; /** * Constructor. */ public WhoisListener() { super(IdentityManager.this.irc, IdentityManager.this.connectionState); } /** * On receiving a server numeric message. * * @param msg Server numeric message */ @Override public void onServerNumericMessage(final ServerNumericMessage msg) { switch (msg.getNumericCode().intValue()) { case RPL_WHOISUSER: final String whoismsg = msg.getText(); final int endNickIndex = whoismsg.indexOf(' '); final String nick = whoismsg.substring(0, endNickIndex); if (!localUser(nick)) { // We can only use WHOIS info about ourselves to discover // our identity on the IRC server. So skip other WHOIS // replies. return; } updateIdentity(whoismsg); // Once the WHOIS reply is processed and the identity is // updated, we can delete the listener as the purpose is // fulfilled. this.irc.deleteListener(this); break; default: break; } } /** * Update the identity container instance with received WHOIS data. * * @param whoismsg the WHOIS reply message content */ private void updateIdentity(final String whoismsg) { final int endNickIndex = whoismsg.indexOf(' '); final int endUserIndex = whoismsg.indexOf(' ', endNickIndex + 1); final int endHostIndex = whoismsg.indexOf(' ', endUserIndex + 1); final String user = whoismsg.substring(endNickIndex + 1, endUserIndex); final String host = whoismsg.substring(endUserIndex + 1, endHostIndex); IdentityManager.this.identity.setHost(host); IdentityManager.this.identity.setUser(user); LOGGER.debug(String.format("Current identity: %s!%s@%s", this.connectionState.getNickname(), user, host)); } } /** * General listener for identity-related events. * * @author Danny van Heumen */ private final class IdentityListener extends AbstractIrcMessageListener { /** * Constructor. */ public IdentityListener() { super(IdentityManager.this.irc, IdentityManager.this.connectionState); } /** * Nick change event. * * @param msg nick change event message */ @Override public void onNickChange(final NickMessage msg) { if (msg == null || msg.getSource() == null) { return; } final String oldNick = msg.getSource().getNick(); final String newNick = msg.getNewNick(); if (oldNick == null || newNick == null) { LOGGER.error("Incomplete nick change message. Old nick: '" + oldNick + "', new nick: '" + newNick + "'."); return; } IdentityManager.this.provider.getPersistentPresence().updateNick( oldNick, newNick); } } /** * Storage container for identity components. * * IRC identity components user and host are stored. The nick name component * isn't stored, because it changes too frequently. When getting the * identity string, the nick name component is provided at calling time. * * @author Danny van Heumen */ private static final class Identity { /** * User name. */ private String user = null; /** * Host name. */ private String host = null; /** * Set user. * * @param user the new user */ private void setUser(final String user) { this.user = user; } /** * Set host. * * @param host the new host */ private void setHost(final String host) { this.host = host; } /** * Check whether or not the Identity information is set. * * @return returns true if identity is set, or false * if it is not (yet) set. */ private boolean isSet() { return this.user != null && this.host != null; } /** * Get identity string. * * @param currentNick the current nick * @return returns identity string */ private String getString(final String currentNick) { return String.format("%s!%s@%s", currentNick, this.user, this.host); } } }