/* * 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.nio.channels.*; import java.security.*; import java.security.cert.*; import java.util.*; import java.util.concurrent.atomic.*; import javax.net.ssl.*; import net.java.sip.communicator.service.certificate.*; 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.*; import com.ircclouds.irc.api.domain.messages.interfaces.*; import com.ircclouds.irc.api.listeners.*; /** * An implementation of IRC using the irc-api library. * * TODO Correctly disconnect IRC connection upon quitting. * * @author Danny van Heumen */ public class IrcStack implements IrcConnectionListener { /** * Logger. */ private static final Logger LOGGER = Logger.getLogger(IrcStack.class); /** * Parent provider for IRC. */ private final ProtocolProviderServiceIrcImpl provider; /** * Server parameters that are set and provided during the connection * process. */ private final ServerParameters params; /** * The persistent context that will survive (dis)connects. */ private final PersistentContext context; /** * Instance of the irc connection contained in an AtomicReference. * * This field serves 2 purposes: * * First is the container itself that we use to synchronize on while * (dis)connecting and eventually setting new instance variable before * unlocking. By synchronizing we have connect and disconnect operations * wait for each other. * * Second is to get the current connection instance. AtomicReference ensures * that we either get the old or the new instance. */ private final AtomicReference<IrcConnection> session = new AtomicReference<IrcConnection>(null); /** * Constructor. * * @param parentProvider Parent provider * @param nick User's nick name * @param login User's login name * @param version Version * @param finger Finger */ public IrcStack(final ProtocolProviderServiceIrcImpl parentProvider, final String nick, final String login, final String version, final String finger) { if (parentProvider == null) { throw new NullPointerException("parentProvider cannot be null"); } this.provider = parentProvider; this.params = new IrcStack.ServerParameters(nick, login, finger, null); this.context = new PersistentContext(this.provider); } /** * Connect to specified host, port, optionally using a password. * * @param host IRC server's host name * @param port IRC port * @param password password for the specified nick name * @param secureConnection true to set up secure connection, or false if * not. * @param autoNickChange do automatic nick changes if nick is in use * @param config Client configuration * @throws OperationFailedException in case of user canceling because of * certificate errors * @throws Exception throws exceptions */ public void connect(final String host, final int port, final String password, final boolean secureConnection, final boolean autoNickChange, final ClientConfig config) throws OperationFailedException, Exception { final String plainPass = determinePlainPassword(password, config); final IRCServer server = createServer(config, host, port, secureConnection, plainPass); try { synchronized (this.session) { final IrcConnection current = this.session.get(); if (current != null && current.isConnected()) { return; } this.params.setServer(server); final IRCApi irc = new IRCApiImpl(true); if (LOGGER.isTraceEnabled()) { // If tracing is enabled, register another listener that // logs all IRC messages as published by the IRC client // library. irc.addListener(new DebugListener()); } // Synchronized IRCApi instance passed on to the connection // instance. this.session.set(new IrcConnection(this.context, config, irc, this.params, password, this)); this.provider.setCurrentRegistrationState( RegistrationState.REGISTERED, RegistrationStateChangeEvent.REASON_USER_REQUEST); } } catch (IOException e) { if (isCausedByCertificateException(e)) { LOGGER.info("Connection aborted due to server certificate."); // If it is caused by a certificate exception, it is because the // user doesn't trust the certificate. Set to unregistered // instead of indicating a failure to connect. this.provider.setCurrentRegistrationState( RegistrationState.UNREGISTERED, RegistrationStateChangeEvent.REASON_USER_REQUEST); throw new OperationFailedException( "Failed certificate verification.", OperationFailedException.OPERATION_CANCELED); } else { // SSL exceptions will be caught here too. this.provider.setCurrentRegistrationState( RegistrationState.CONNECTION_FAILED, RegistrationStateChangeEvent.REASON_NOT_SPECIFIED); throw e; } } catch (InterruptedException e) { this.provider.setCurrentRegistrationState( RegistrationState.UNREGISTERED, RegistrationStateChangeEvent.REASON_USER_REQUEST); throw e; } catch (NotYetConnectedException e) { this.provider.setCurrentRegistrationState( RegistrationState.CONNECTION_FAILED, RegistrationStateChangeEvent.REASON_NOT_SPECIFIED); throw e; } catch (Exception e) { // For any other (unexpected error) first log the error itself for // debugging purposes. Then rethrow. LOGGER.error("Unanticipated exception occurred!", e); this.provider.setCurrentRegistrationState( RegistrationState.CONNECTION_FAILED, RegistrationStateChangeEvent.REASON_INTERNAL_ERROR); throw e; } } /** * Create matching IRCServer instances based on connection parameters. * * @param config the IRC config * @param host the IRC server host * @param port the IRC server port * @param secureConnection <tt>true</tt> for a secure connection, * <tt>false</tt> for plain text connection * @param password the normal IRC password (<tt>Note</tt> this is not the * password used for SASL authentication. This password may be * null in case SASL authentication is required.) * @return Returns a server instance that matches the provided parameters. */ private IRCServer createServer(final ClientConfig config, final String host, final int port, final boolean secureConnection, final String password) { final IRCServer server; if (secureConnection) { server = new SecureIRCServer(host, port, password, getCustomSSLContext(host), config.getProxy(), config.isResolveByProxy()); } else { server = new IRCServer(host, port, password, false, config.getProxy(), config.isResolveByProxy()); } return server; } /** * Determine the correct plain IRC password for the provided IRC * configuration. * * @param password the user-specified password * @param config the IRC configuration, which includes possible SASL * preferences * @return Returns the IRC plain password to use in the connection, * determined by the provided IRC configuration. */ private String determinePlainPassword(final String password, final ClientConfig config) { final String plainPass; if (config.isVersion3Allowed() && config.getSASL() != null) { plainPass = null; } else { plainPass = password; } return plainPass; } /** * Check to see if a certificate exception is the root cause for the * exception. * * @param e the exception * @return returns <tt>true</tt> if certificate exception is root cause, or * <tt>false</tt> otherwise. */ private boolean isCausedByCertificateException(final Exception e) { Throwable cause = e; while (cause != null) { if (cause instanceof CertificateException) { return true; } cause = cause.getCause(); } return false; } /** * Get the current connection instance. * * @return returns current connection instance or null if no connection is * established. */ public IrcConnection getConnection() { return this.session.get(); } /** * Get the stack's persistent context instance. * * @return returns this stack's persistent context instance */ PersistentContext getContext() { return this.context; } /** * Create a custom SSL context for this particular server. * * @param hostname host name of the host we are connecting to such that we * can verify that the same host name is on the server * certificate * @return returns a customized SSL context or <tt>null</tt> if one cannot * be created. */ private SSLContext getCustomSSLContext(final String hostname) { SSLContext context = null; try { CertificateService cs = IrcActivator.getCertificateService(); X509TrustManager tm = cs.getTrustManager(hostname); context = cs.getSSLContext(tm); } catch (GeneralSecurityException e) { LOGGER.error("failed to create custom SSL context", e); } return context; } /** * Disconnect from the IRC server. */ public void disconnect() { final IrcConnection connection; synchronized (this.session) { // synchronization needed to ensure that no other process (such as // connection attempt) is in progress // Set session to null first, such that we can identify that we // disconnect intentionally. connection = this.session.getAndSet(null); if (connection != null) { connection.disconnect(); } } this.provider.setCurrentRegistrationState( RegistrationState.UNREGISTERED, RegistrationStateChangeEvent.REASON_USER_REQUEST); } /** * Dispose. */ public void dispose() { disconnect(); } /** * Listener for debugging purposes. If logging level is set high enough, * this listener is added to the irc-api client so it can show all IRC * messages as they are handled. * * <p> * This listener is <em>intentionally</em> not deleted upon disconnect * (ERROR or QUIT), for purpose of tracking any remaining activity that may * occur in case of a implementation issue. * </p> * * @author Danny van Heumen */ private static final class DebugListener implements IMessageListener { /** * {@inheritDoc} */ @Override public void onMessage(final IMessage aMessage) { LOGGER.trace("(" + aMessage + ") " + aMessage.asRaw()); } } /** * Container for storing server parameters. * * @author Danny van Heumen */ private static final class ServerParameters implements IServerParameters { /** * Number of increments to try for alternative nick names. */ private static final int NUM_INCREMENTS_FOR_ALTERNATIVES = 10; /** * Nick name. */ private String nick; /** * Alternative nick names. */ private List<String> alternativeNicks = new ArrayList<String>(); /** * Real name. */ private String real; /** * Ident. */ private String ident; /** * IRC server. */ private IRCServer server; /** * Construct ServerParameters instance. * @param nickName nick name * @param realName real name * @param ident ident * @param server IRC server instance */ private ServerParameters(final String nickName, final String realName, final String ident, final IRCServer server) { this.nick = IdentityManager.checkNick(nickName, null); this.alternativeNicks.add(nickName + "_"); this.alternativeNicks.add(nickName + "__"); this.alternativeNicks.add(nickName + "___"); this.alternativeNicks.add(nickName + "____"); for (int i = 1; i < NUM_INCREMENTS_FOR_ALTERNATIVES; i++) { this.alternativeNicks.add(nickName + i); } this.real = realName; this.ident = ident; this.server = server; } /** * Get nick name. * * @return returns nick name */ @Override public String getNickname() { return this.nick; } /** * Get alternative nick names. * * @return returns list of alternatives */ @Override public List<String> getAlternativeNicknames() { return this.alternativeNicks; } /** * Get ident string. * * @return returns ident */ @Override public String getIdent() { return this.ident; } /** * Get real name. * * @return returns real name */ @Override public String getRealname() { return this.real; } /** * Get server. * * @return returns server instance */ @Override public IRCServer getServer() { return this.server; } /** * Set server instance. * * @param server IRC server instance */ public void setServer(final IRCServer server) { if (server == null) { throw new IllegalArgumentException("server cannot be null"); } this.server = server; } } /** * Event for any kind of connection interruption, including normal QUIT * events. * * @param connection the connection that gets interrupted */ @Override public void connectionInterrupted(final IrcConnection connection) { // Disconnected sessions are nulled before disconnect() is called. Hence // we can detect by IrcConnection instance contained in the session // whether or not the connection interruption is unintended. if (this.session.get() != connection) { // Interruption was intended: instance either nulled or a new // instance is already set. LOGGER.debug("Interrupted connection is not the current connection" + ", so assuming that connection interruption was intended."); return; } LOGGER.warn("IRC connection interrupted unexpectedly."); this.provider.setCurrentRegistrationState( RegistrationState.CONNECTION_FAILED, RegistrationStateChangeEvent.REASON_NOT_SPECIFIED); } /** * Persistent context that is used to survive (dis)connects. * * @author Danny van Heumen */ static final class PersistentContext { /** * The protocol provider service instance. */ final ProtocolProviderServiceIrcImpl provider; /** * The nick watch list as a SYNCHRONIZED sorted set. */ final SortedSet<String> nickWatchList = Collections .synchronizedSortedSet(new TreeSet<String>()); /** * Private constructor to ensure use only by IrcStack itself. * * @param provider the provider instance */ private PersistentContext(final ProtocolProviderServiceIrcImpl provider) { if (provider == null) { throw new IllegalArgumentException("provider cannot be null"); } this.provider = provider; } } }