/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.protocol.irc; import java.io.*; import java.util.*; import net.java.sip.communicator.impl.protocol.irc.ClientConfig.SASL; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.event.*; import net.java.sip.communicator.util.*; import com.ircclouds.irc.api.*; import com.ircclouds.irc.api.domain.messages.*; import com.ircclouds.irc.api.listeners.*; import com.ircclouds.irc.api.negotiators.*; import com.ircclouds.irc.api.negotiators.CompositeNegotiator.Capability; import com.ircclouds.irc.api.negotiators.capabilities.*; import com.ircclouds.irc.api.state.*; /** * IRC Connection. * * TODO Show MOTD in Jitsi "System Room" or something similar, since the MOTD is * aimed directly at the local user. * * @author Danny van Heumen */ public class IrcConnection { /** * Logger. */ private static final Logger LOGGER = Logger.getLogger(IrcConnection.class); /** * Set of characters with special meanings for IRC, such as: ',' used as * separator of list of items (channels, nicks, etc.), ' ' (space) separator * of command parameters, etc. */ public static final Set SPECIAL_CHARACTERS; /** * Initialize set of special characters. */ static { HashSet specials = new HashSet(); specials.add('\0'); specials.add('\n'); specials.add('\r'); specials.add(' '); specials.add(','); SPECIAL_CHARACTERS = Collections.unmodifiableSet(specials); } /** * Context. */ private final IrcStack.PersistentContext context; /** * IRC client configuration for managing (more advanced) client behaviour * such as the use of periodic tasks for querying presence of contacts and * channel members. */ private final ClientConfig config; /** * IRC Api instance. */ private final IRCApi irc; /** * Connection state of a successful IRC connection. */ private final IIRCState connectionState; /** * Callback to inform on connection interruptions. */ private final IrcConnectionListener connectionListener; /** * Manager component that manages current IRC presence. */ private final PresenceManager presence; /** * Manager component for server channel listing. */ private final ServerChannelLister channelLister; /** * The local user's identity as it will be used in server-client * communication for sent messages. */ private final IdentityManager identity; /** * The channel manager instance. */ private final ChannelManager channel; /** * The message manager instance. */ private final MessageManager message; /** * Constructor. * * @param context persistent context that crosses connections * @param config client configuration * @param irc the irc instance * @param params connection parameters * @param password the password for authentication * @param connectionListener listener for callback upon connection * interruption * @param allowV3 Allow IRC version 3 capability negotiation. If not * allowed, this may regress the IRC client to "classic" IRC * (RFC1459) * @throws Exception Throws IOException in case of connection problems. */ IrcConnection(final IrcStack.PersistentContext context, final ClientConfig config, final IRCApi irc, final IServerParameters params, final String password, final IrcConnectionListener connectionListener) throws Exception { if (context == null) { throw new IllegalArgumentException("context cannot be null"); } this.context = context; if (config == null) { throw new IllegalArgumentException("client config cannot be null"); } this.config = config; if (irc == null) { throw new IllegalArgumentException("irc instance cannot be null"); } this.irc = irc; this.connectionListener = connectionListener; // Prepare an IRC capability negotiator in case version 3 is allowed. final CapabilityNegotiator negotiator; final NegotiationHandler handler = new NegotiationHandler(); if (config.isVersion3Allowed()) { negotiator = determineNegotiator(params.getNickname(), password, config, handler); } else { negotiator = null; } // Install a listener for everything that is not directly related to a // specific chat room or operation. this.irc.addListener(new ServerListener()); // Now actually connect to the IRC server. this.connectionState = connectSynchronized(this.context.provider, params, this.irc, negotiator); // instantiate identity manager for the connection this.identity = new IdentityManager(this.irc, this.connectionState, this.context.provider); // instantiate message manager for the connection this.message = new MessageManager(this, this.irc, this.connectionState, this.context.provider, this.identity); // instantiate channel manager for the connection this.channel = new ChannelManager(this.irc, this.connectionState, this.context.provider, this.config, handler.awayNotify); // instantiate presence manager for the connection this.presence = new PresenceManager(this.irc, this.connectionState, this.context.provider.getPersistentPresence(), this.config, this.context.nickWatchList); // instantiate server channel lister this.channelLister = new ServerChannelLister(this.irc, this.connectionState); } /** * Determine which capability negotiator needed. * * Decide on which capability negotiator will be used in IRC server * registration. The null negotiator is used to skip negotiation completely. * This may regress the client connection to plain IRC (RFC1459) as defined * in the specification * (http://ircv3.atheme.org/specification/capability-negotiation-3.1). * * The NoopNegotiator should be used to do IRCv3 negotiation (enabling IRCv3 * in the process) but not set up anything at that moment. * * @param user the user nick used for authentication * @param password the authentication password * @param config the client configuration * @param handler the negotiation handler for updates during negotiation * @return returns capability negotiator */ private static CapabilityNegotiator determineNegotiator(final String user, final String password, final ClientConfig config, final CompositeNegotiator.Host handler) { final ArrayList capabilities = new ArrayList(); capabilities.add(new SimpleCapability("away-notify")); capabilities.add(new SimpleCapability("multi-prefix")); final SASL sasl = config.getSASL(); if (sasl != null) { capabilities.add(new SaslCapability(true, sasl.getRole(), sasl .getUser(), sasl.getPass())); } return new CompositeNegotiator(capabilities, handler); } /** * Perform synchronized connect operation. * * @param provider Parent protocol provider * @param params Server connection parameters * @param irc IRC Api instance * @param negotiator the capability negotiator for enabling IRCv3 features * @throws Exception exception thrown when connect fails */ private static IIRCState connectSynchronized( final ProtocolProviderServiceIrcImpl provider, final IServerParameters params, final IRCApi irc, final CapabilityNegotiator negotiator) throws Exception { final Result result = new Result(); synchronized (result) { // start connecting to the specified server ... irc.connect(params, new Callback() { @Override public void onSuccess(final IIRCState state) { synchronized (result) { LOGGER.trace("IRC connected successfully!"); result.setDone(state); result.notifyAll(); } } @Override public void onFailure(final Exception e) { synchronized (result) { LOGGER.trace("IRC connection FAILED!", e); result.setDone(e); result.notifyAll(); } } }, negotiator); provider.setCurrentRegistrationState(RegistrationState.REGISTERING, RegistrationStateChangeEvent.REASON_USER_REQUEST); while (!result.isDone()) { LOGGER.trace("Waiting for the connection to be " + "established ..."); result.wait(); } } // TODO Implement connection timeout and a way to recognize that // the timeout occurred. final Exception e = result.getException(); if (e != null) { throw new IOException(e); } final IIRCState state = result.getValue(); if (state == null) { throw new IOException( "Failed to connect to IRC server: connection state is null"); } return state; } /** * Check whether or not a connection is established. * * @return true if connected, false otherwise. */ public boolean isConnected() { return this.connectionState != null && this.connectionState.isConnected(); } /** * Check whether the connection is a secure connection (TLS). * * @return true if connection is secure, false otherwise. */ public boolean isSecureConnection() { return isConnected() && this.connectionState.getServer().isSSL(); } /** * Disconnect. */ void disconnect() { try { this.irc.disconnect(); } catch (RuntimeException e) { // Disconnect might throw ChannelClosedException. Shouldn't be a // problem, but for now lets log it just to be sure. LOGGER.debug("exception occurred while disconnecting", e); } } /** * Get the IRC client library instance. * * @return returns the client instance */ public IRCApi getClient() { return this.irc; } /** * Get the presence manager. (Guaranteed to be non-null.) * * @return returns the presence manager instance */ public PresenceManager getPresenceManager() { return this.presence; } /** * Get the channel lister that facilitates server channel queries. * (Guaranteed to be non-null.) * * @return returns the channel lister instance */ public ServerChannelLister getServerChannelLister() { return this.channelLister; } /** * Get the identity manager instance. (Guaranteed to be non-null.) * * @return returns the identity manager instance */ public IdentityManager getIdentityManager() { return this.identity; } /** * Get the channel manager instance. (Guaranteed to be non-null.) * * @return returns the channel manager instance */ public ChannelManager getChannelManager() { return this.channel; } /** * Get the message manager instance. (Guaranteed to be non-null.) * * @return returns the message manager instance */ public MessageManager getMessageManager() { return this.message; } /** * A listener for server-level messages (any messages that are related to * the server, the connection, that are not related to any chatroom in * particular) or that are personal message from user to local user. */ private final class ServerListener extends VariousMessageListenerAdapter { /** * Print out server notices for debugging purposes and for simply * keeping track of the connections. * * @param msg the server notice */ @Override public void onServerNotice(final ServerNotice msg) { LOGGER.debug("NOTICE: " + msg.getText()); } /** * Print out received errors for debugging purposes and may be for * expected errors that can be acted upon. * * @param msg the error message */ @Override public void onError(final ErrorMessage msg) { if (LOGGER.isDebugEnabled()) { LOGGER .debug("SERVER ERROR: " + msg.getSource() + ": " + msg.getText()); } // Errors signal fatal situation, so unregister and assume // connection lost. LOGGER.debug("Local user received ERROR message: removing server " + "listener."); IrcConnection.this.irc.deleteListener(this); // If listener is available, inform of connection interrupt. if (IrcConnection.this.connectionListener != null) { IrcConnection.this.connectionListener .connectionInterrupted(IrcConnection.this); } } /** * Received Client error for "fatal" connectivity issues. * * In case of client-side discovered disruptive connectivity issues, we * need to inform listeners, as the IRC server will not be able to do so * anymore. * * @param msg the client-side error message */ @Override public void onClientError(ClientErrorMessage msg) { if (LOGGER.isDebugEnabled()) { LOGGER.debug( "CLIENT ERROR: " + msg.getException().getMessage(), msg.getException()); } // Errors signal fatal situation, so unregister and assume // connection lost. LOGGER.debug("Local user received CLIENT ERROR message: removing " + "server listener."); IrcConnection.this.irc.deleteListener(this); // If listener is available, inform of connection interrupt. if (IrcConnection.this.connectionListener != null) { IrcConnection.this.connectionListener .connectionInterrupted(IrcConnection.this); } } /** * User quit messages. * * User quit messages only need to be handled in case quitting users, * since that is the only clear signal of presence change we have. * * @param msg Quit message */ @Override public void onUserQuit(final QuitMessage msg) { final String user = msg.getSource().getNick(); if (!IrcConnection.this.connectionState.getNickname().equals(user)) { return; } LOGGER.debug("Local user's QUIT message received: removing " + "server listener."); IrcConnection.this.irc.deleteListener(this); // If listener is available, inform of connection interrupt. if (IrcConnection.this.connectionListener != null) { IrcConnection.this.connectionListener .connectionInterrupted(IrcConnection.this); } } } /** * Capability negotiation handler. * * This handler receives the negotiation results for each capability as soon * as it is known. This class is used to get an update on what capabilities * are available to the client. * * @author Danny van Heumen */ private static class NegotiationHandler implements CompositeNegotiator.Host { /** * Constant for id of away notify capability. */ private static final String AWAY_NOTIFY = "away-notify"; /** * Availability of 'away-notify' capability. */ private boolean awayNotify = false; @Override public void acknowledge(Capability cap) { LOGGER.info("Capability " + cap.getId() + " acknowledged."); if (AWAY_NOTIFY.equals(cap.getId())) { this.awayNotify = true; } } @Override public void reject(Capability cap) { LOGGER.info("Capability " + cap.getId() + " rejected."); if (AWAY_NOTIFY.equals(cap.getId())) { this.awayNotify = false; } } } }