/* * Copyright (c) 2006-2012 DMDirc Developers * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.dmdirc.parser.irc; import com.dmdirc.parser.common.BaseParser; import com.dmdirc.parser.common.ChannelJoinRequest; import com.dmdirc.parser.common.ChildImplementations; import com.dmdirc.parser.common.CompositionState; import com.dmdirc.parser.common.IgnoreList; import com.dmdirc.parser.common.MyInfo; import com.dmdirc.parser.common.ParserError; import com.dmdirc.parser.common.QueuePriority; import com.dmdirc.parser.common.SRVRecord; import com.dmdirc.parser.common.SystemEncoder; import com.dmdirc.parser.interfaces.Encoder; import com.dmdirc.parser.interfaces.EncodingParser; import com.dmdirc.parser.interfaces.SecureParser; import com.dmdirc.parser.interfaces.callbacks.*; //NOPMD import com.dmdirc.parser.irc.IRCReader.ReadLine; import com.dmdirc.parser.irc.outputqueue.OutputQueue; import java.io.IOException; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Timer; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** * IRC Parser. */ @ChildImplementations({ IRCChannelClientInfo.class, IRCChannelInfo.class, IRCClientInfo.class }) public class IRCParser extends BaseParser implements SecureParser, EncodingParser { /** Max length an outgoing line should be (NOT including \r\n). */ public static final int MAX_LINELENGTH = 510; /** General Debug Information. */ public static final int DEBUG_INFO = 1; /** Socket Debug Information. */ public static final int DEBUG_SOCKET = 2; /** Processing Manager Debug Information. */ public static final int DEBUG_PROCESSOR = 4; /** List Mode Queue Debug Information. */ public static final int DEBUG_LMQ = 8; /** Attempt to update user host all the time, not just on Who/Add/NickChange. */ static final boolean ALWAYS_UPDATECLIENT = true; /** Byte used to show that a non-boolean mode is a list (b). */ static final byte MODE_LIST = 1; /** Byte used to show that a non-boolean mode is not a list, and requires a parameter to set (lk). */ static final byte MODE_SET = 2; /** Byte used to show that a non-boolean mode is not a list, and requires a parameter to unset (k). */ static final byte MODE_UNSET = 4; /** * This is what the user wants settings to be. * Nickname here is *not* always accurate.

* ClientInfo variable tParser.getMyself() should be used for accurate info. */ private MyInfo me = new MyInfo(); /** Should PINGs be sent to the server to check if its alive? */ private boolean checkServerPing = true; /** Timer for server ping. */ private Timer pingTimer = null; /** Semaphore for access to pingTimer. */ private final Semaphore pingTimerSem = new Semaphore(1); /** Is a ping needed? */ private final AtomicBoolean pingNeeded = new AtomicBoolean(false); /** Time last ping was sent at. */ private long pingTime; /** Current Server Lag. */ private long serverLag; /** Last value sent as a ping argument. */ private String lastPingValue = ""; /** * Count down to next ping. * The timer fires every 10 seconds, this value is decreased every time the * timer fires.
* Once it reaches 0, we send a ping, and reset it to 6, this means we ping * the server every minute. * * @see setPingCountDownLength */ private int pingCountDown; /** Network name. This is "" if no network name is provided */ String networkName; /** This is what we think the nickname should be. */ String thinkNickname; /** When using inbuilt pre-001 NickInUse handler, have we tried our AltNick. */ boolean triedAlt; /** Have we received the 001. */ boolean got001; /** Have we fired post005? */ boolean post005; /** Has the thread started execution yet, (Prevents run() being called multiple times). */ boolean hasBegan; /** Connect timeout. */ private int connectTimeout = 5000; /** Hashtable storing known prefix modes (ohv). */ final Map prefixModes = new HashMap(); /** * Hashtable maping known prefix modes (ohv) to prefixes (@%+) - Both ways. * Prefix map contains 2 pairs for each mode. (eg @ => o and o => @) */ final Map prefixMap = new HashMap(); /** Integer representing the next avaliable integer value of a prefix mode. */ long nextKeyPrefix = 1; /** Hashtable storing known user modes (owxis etc). */ final Map userModes = new HashMap(); /** Integer representing the next avaliable integer value of a User mode. */ long nNextKeyUser = 1; /** * Hashtable storing known boolean chan modes (cntmi etc). * Valid Boolean Modes are stored as Hashtable.pub('m',1); where 'm' is the mode and 1 is a numeric value.

* Numeric values are powers of 2. This allows up to 32 modes at present (expandable to 64)

* ChannelInfo/ChannelClientInfo etc provide methods to view the modes in a human way.

*
* Channel modes discovered but not listed in 005 are stored as boolean modes automatically (and a ERROR_WARNING Error is called) */ final Map chanModesBool = new HashMap(); /** Integer representing the next avaliable integer value of a Boolean mode. */ long nextKeyCMBool = 1; /** * Hashtable storing known non-boolean chan modes (klbeI etc). * Non Boolean Modes (for Channels) are stored together in this hashtable, the value param * is used to show the type of variable. (List (1), Param just for set (2), Param for Set and Unset (2+4=6))

*
* see MODE_LIST
* see MODE_SET
* see MODE_UNSET
*/ final Map chanModesOther = new HashMap(); /** The last line of input received from the server */ private ReadLine lastLine = null; /** Should the lastline (where given) be appended to the "data" part of any onErrorInfo call? */ private boolean addLastLine = false; /** Channel Prefixes (ie # + etc). */ private final List chanPrefix = Collections.synchronizedList(new LinkedList()); /** Hashtable storing all known clients based on nickname (in lowercase). */ private final Map clientList = new HashMap(); /** Hashtable storing all known channels based on chanel name (inc prefix - in lowercase). */ private final Map channelList = new HashMap(); /** Reference to the ClientInfo object that references ourself. */ private IRCClientInfo myself = new IRCClientInfo(this, "myself").setFake(true); /** Hashtable storing all information gathered from 005. */ final Map h005Info = new HashMap(); /** difference in ms between our time and the servers time (used for timestampedIRC). */ long tsdiff; /** Reference to the Processing Manager. */ private final ProcessingManager myProcessingManager = new ProcessingManager(this); /** Should we automatically disconnect on fatal errors?. */ private boolean disconnectOnFatal = true; /** Current Socket State. */ protected SocketState currentSocketState = SocketState.NULL; /** This is the socket used for reading from/writing to the IRC server. */ private Socket socket; /** * The underlying socket used for reading/writing to the IRC server. * For normal sockets this will be the same as {@link #socket} but for SSL * connections this will be the underlying {@link Socket} while * {@link #socket} will be an {@link SSLSocket}. */ private Socket rawSocket; /** Used for writing to the server. */ private OutputQueue out; /** The encoder to use to encode incoming lines. */ private Encoder encoder = new SystemEncoder(); /** Used for reading from the server. */ private IRCReader in; /** This is the default TrustManager for SSL Sockets, it trusts all ssl certs. */ private final TrustManager[] trustAllCerts = { new X509TrustManager() { @Override public X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted(final X509Certificate[] certs, final String authType) { } @Override public void checkServerTrusted(final X509Certificate[] certs, final String authType) { } },}; /** Should channels automatically request list modes? */ private boolean autoListMode = true; /** Should part/quit/kick callbacks be fired before removing the user internally? */ private boolean removeAfterCallback = true; /** This is the TrustManager used for SSL Sockets. */ private TrustManager[] myTrustManager = trustAllCerts; /** The KeyManagers used for client certificates for SSL sockets. */ private KeyManager[] myKeyManagers; /** This is list containing 001 - 005 inclusive. */ private final List serverInformationLines = new LinkedList(); /** Map of capabilities and their state. */ private final Map capabilities = new HashMap(); /** * Default constructor, ServerInfo and MyInfo need to be added separately (using IRC.me and IRC.server). */ public IRCParser() { this((MyInfo) null); } /** * Constructor with ServerInfo, MyInfo needs to be added separately (using IRC.me). * * @param uri The URI to connect to */ public IRCParser(final URI uri) { this(null, uri); } /** * Constructor with MyInfo, ServerInfo needs to be added separately (using IRC.server). * * @param myDetails Client information. */ public IRCParser(final MyInfo myDetails) { this(myDetails, null); } /** * Creates a new IRCParser with the specified client details which will * connect to the specified URI. * * @since 0.6.3 * @param myDetails The client details to use * @param uri The URI to connect to */ public IRCParser(final MyInfo myDetails, final URI uri) { super(uri); out = new OutputQueue(); if (myDetails != null) { this.me = myDetails; } setIgnoreList(new IgnoreList()); setPingTimerInterval(10000); setPingTimerFraction(6); resetState(); } /** * Get the current OutputQueue * * @return the current OutputQueue */ public OutputQueue getOutputQueue() { return out; } /** {@inheritDoc} */ @Override public boolean compareURI(final URI uri) { // Get the old URI. final URI oldURI = getURI(); final URI newURI = uri; // Check that protocol, host and port are the same. // Anything else won't change the server we connect to just what we // would do after connecting, so is not relevent. return newURI.getScheme().equalsIgnoreCase(oldURI.getScheme()) && newURI.getHost().equalsIgnoreCase(oldURI.getHost()) && ((newURI.getUserInfo() == null || newURI.getUserInfo().isEmpty()) || newURI.getUserInfo().equalsIgnoreCase((oldURI.getUserInfo() == null ? "" : oldURI.getUserInfo()))) && newURI.getPort() == oldURI.getPort(); } /** * From the given URI, get a URI to actually connect to. * This function will check for DNS SRV records for the given URI and use * those if found. * If no SRV records exist, then fallback to using the URI as-is but with * a default port specified if none is given. * * @param uri Requested URI. * @return A connectable version of the given URI. */ private URI getConnectURI(final URI uri) { if (uri == null) { return null; } final boolean isSSL = uri.getScheme().endsWith("s"); final int defaultPort = isSSL ? 6697 : 6667; // Default to what the URI has already.. int port = uri.getPort(); String host = uri.getHost(); // Look for SRV records if no port is specified. if (port == -1) { List recordList = new ArrayList(); if (isSSL) { // There are a few possibilities for ssl... final String[] protocols = {"_ircs._tcp.", "_irc._tls."}; for (final String protocol : protocols) { recordList = SRVRecord.getRecords(protocol + host); if (!recordList.isEmpty()) { break; } } } else { recordList = SRVRecord.getRecords("_irc._tcp." + host); } if (!recordList.isEmpty()) { host = recordList.get(0).getHost(); port = recordList.get(0).getPort(); } } // Fix the port if required. if (port == -1) { port = defaultPort; } // Return the URI to connect to based on the above. try { return new URI(uri.getScheme(), uri.getUserInfo(), host, port, uri.getPath(), uri.getQuery(), uri.getFragment()); } catch (URISyntaxException ex) { // Shouldn't happen - but return the URI as-is if it does. return uri; } } /** {@inheritDoc} */ @Override public Collection extractChannels(final URI uri) { if (uri == null) { return Collections.emptyList(); } String channelString = uri.getPath(); if (uri.getRawQuery() != null && !uri.getRawQuery().isEmpty()) { channelString += "?" + uri.getRawQuery(); } if (uri.getRawFragment() != null && !uri.getRawFragment().isEmpty()) { channelString += "#" + uri.getRawFragment(); } if (!channelString.isEmpty() && channelString.charAt(0) == '/') { channelString = channelString.substring(1); } return extractChannels(channelString); } /** * Extracts a set of channels and optional keys from the specified String. * Channels are separated by commas, and keys are separated from their * channels by a space. * * @since 0.6.4 * @param channels The string of channels to parse * @return A corresponding collection of join request objects */ protected Collection extractChannels(final String channels) { final List res = new ArrayList(); for (String channel : channels.split(",")) { final String[] parts = channel.split(" ", 2); if (parts.length == 2) { res.add(new ChannelJoinRequest(parts[0], parts[1])); } else { res.add(new ChannelJoinRequest(parts[0])); } } return res; } /** * Get the current Value of autoListMode. * * @return Value of autoListMode (true if channels automatically ask for list modes on join, else false) */ public boolean getAutoListMode() { return autoListMode; } /** * Set the current Value of autoListMode. * * @param newValue New value to set autoListMode */ public void setAutoListMode(final boolean newValue) { autoListMode = newValue; } /** * Get the current Value of removeAfterCallback. * * @return Value of removeAfterCallback (true if kick/part/quit callbacks are fired before internal removal) */ public boolean getRemoveAfterCallback() { return removeAfterCallback; } /** * Get the current Value of removeAfterCallback. * * @param newValue New value to set removeAfterCallback */ public void setRemoveAfterCallback(final boolean newValue) { removeAfterCallback = newValue; } /** * Get the current Value of addLastLine. * * @return Value of addLastLine (true if lastLine info will be automatically * added to the errorInfo data line). This should be true if lastLine * isn't handled any other way. */ public boolean getAddLastLine() { return addLastLine; } /** * Get the current Value of addLastLine. * * @param newValue New value to set addLastLine */ public void setAddLastLine(final boolean newValue) { addLastLine = newValue; } /** * Get the current Value of connectTimeout. * * @return The value of getConnectTimeout. */ public int getConnectTimeout() { return connectTimeout; } /** * Set the Value of connectTimeout. * * @param newValue new value for value of getConnectTimeout. */ public void setConnectTimeout(final int newValue) { connectTimeout = newValue; } /** * Get the current socket State. * * @since 0.6.3m1 * @return Current {@link SocketState} */ public SocketState getSocketState() { return currentSocketState; } /** * Get a reference to the Processing Manager. * * @return Reference to the CallbackManager */ public ProcessingManager getProcessingManager() { return myProcessingManager; } /** * Get a reference to the default TrustManager for SSL Sockets. * * @return a reference to trustAllCerts */ public TrustManager[] getDefaultTrustManager() { return trustAllCerts; } /** * Get a reference to the current TrustManager for SSL Sockets. * * @return a reference to myTrustManager; */ public TrustManager[] getTrustManager() { return myTrustManager; } /** {@inheritDoc} */ @Override public void setTrustManagers(final TrustManager[] managers) { myTrustManager = managers; } /** {@inheritDoc} */ @Override public void setKeyManagers(final KeyManager[] managers) { myKeyManagers = managers; } //--------------------------------------------------------------------------- // Start Callbacks //--------------------------------------------------------------------------- /** * Callback to all objects implementing the ServerError Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IServerError * @param message The error message */ protected void callServerError(final String message) { getCallback(ServerErrorListener.class).onServerError(null, null, message); } /** * Callback to all objects implementing the DataIn Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IDataIn * @param data Incoming Line. */ protected void callDataIn(final String data) { getCallback(DataInListener.class).onDataIn(null, null, data); } /** * Callback to all objects implementing the DataOut Callback. * * @param data Outgoing Data * @param fromParser True if parser sent the data, false if sent using .sendLine * @see com.dmdirc.parser.irc.callbacks.interfaces.IDataOut */ protected void callDataOut(final String data, final boolean fromParser) { getCallback(DataOutListener.class).onDataOut(null, null, data, fromParser); } /** * Callback to all objects implementing the DebugInfo Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IDebugInfo * @param level Debugging Level (DEBUG_INFO, DEBUG_SOCKET etc) * @param data Debugging Information as a format string * @param args Formatting String Options */ protected void callDebugInfo(final int level, final String data, final Object... args) { callDebugInfo(level, String.format(data, args)); } /** * Callback to all objects implementing the DebugInfo Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IDebugInfo * @param level Debugging Level (DEBUG_INFO, DEBUG_SOCKET etc) * @param data Debugging Information */ protected void callDebugInfo(final int level, final String data) { getCallback(DebugInfoListener.class).onDebugInfo(null, null, level, data); } /** * Callback to all objects implementing the IErrorInfo Interface. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IErrorInfo * @param errorInfo ParserError object representing the error. */ protected void callErrorInfo(final ParserError errorInfo) { getCallback(ErrorInfoListener.class).onErrorInfo(null, null, errorInfo); } /** * Callback to all objects implementing the IConnectError Interface. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IConnectError * @param errorInfo ParserError object representing the error. */ protected void callConnectError(final ParserError errorInfo) { getCallback(ConnectErrorListener.class).onConnectError(null, null, errorInfo); } /** * Callback to all objects implementing the SocketClosed Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.ISocketClosed */ protected void callSocketClosed() { getCallback(SocketCloseListener.class).onSocketClosed(null, null); } /** * Callback to all objects implementing the PingFailed Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingFailed * @return True if any callback was called, false otherwise. */ protected boolean callPingFailed() { return getCallbackManager().getCallbackType(PingFailureListener.class).call(); } /** * Callback to all objects implementing the PingSent Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingSent */ protected void callPingSent() { getCallback(PingSentListener.class).onPingSent(null, null); } /** * Callback to all objects implementing the PingSuccess Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingSuccess */ protected void callPingSuccess() { getCallback(PingSuccessListener.class).onPingSuccess(null, null); } /** * Callback to all objects implementing the Post005 Callback. * * @see IPost005 */ protected synchronized void callPost005() { if (post005) { return; } post005 = true; if (!h005Info.containsKey("CHANTYPES")) { parseChanPrefix(); } if (!h005Info.containsKey("PREFIX")) { parsePrefixModes(); } if (!h005Info.containsKey("USERMODES")) { parseUserModes(); } if (!h005Info.containsKey("CHANMODES")) { parseChanModes(); } getCallback(ServerReadyListener.class).onServerReady(null, null); } //--------------------------------------------------------------------------- // End Callbacks //--------------------------------------------------------------------------- /** Reset internal state (use before doConnect). */ private void resetState() { // Reset General State info triedAlt = false; got001 = false; post005 = false; // Clear the hash tables channelList.clear(); clientList.clear(); h005Info.clear(); prefixModes.clear(); prefixMap.clear(); chanModesOther.clear(); chanModesBool.clear(); userModes.clear(); chanPrefix.clear(); // Clear output queue. if (out != null) { out.clearQueue(); } // Reset the mode indexes nextKeyPrefix = 1; nextKeyCMBool = 1; nNextKeyUser = 1; setServerName(""); networkName = ""; lastLine = null; myself = new IRCClientInfo(this, "myself").setFake(true); synchronized (serverInformationLines) { serverInformationLines.clear(); } stopPingTimer(); currentSocketState = SocketState.CLOSED; setEncoding(IRCEncoding.RFC1459); } /** * Called after other error callbacks. * CallbackOnErrorInfo automatically calls this *AFTER* any registered callbacks * for it are called. * * @param errorInfo ParserError object representing the error. * @param called True/False depending on the the success of other callbacks. */ public void onPostErrorInfo(final ParserError errorInfo, final boolean called) { if (errorInfo.isFatal() && disconnectOnFatal) { disconnect("Fatal Parser Error"); } } /** * Get the current Value of disconnectOnFatal. * * @return Value of disconnectOnFatal (true if the parser automatically disconnects on fatal errors, else false) */ public boolean getDisconnectOnFatal() { return disconnectOnFatal; } /** * Set the current Value of disconnectOnFatal. * * @param newValue New value to set disconnectOnFatal */ public void setDisconnectOnFatal(final boolean newValue) { disconnectOnFatal = newValue; } private Socket newSocket(final URI target, final URI proxy) throws IOException { if (target.getPort() > 65535 || target.getPort() <= 0) { throw new IOException("Server port (" + target.getPort() + ") is invalid."); } Socket mySocket = null; if (proxy == null) { callDebugInfo(DEBUG_SOCKET, "Not using Proxy"); mySocket = new Socket(); if (getBindIP() != null && !getBindIP().isEmpty()) { callDebugInfo(DEBUG_SOCKET, "Binding to IP: " + getBindIP()); try { mySocket.bind(new InetSocketAddress(InetAddress.getByName(getBindIP()), 0)); } catch (IOException e) { callDebugInfo(DEBUG_SOCKET, "Binding failed: " + e.getMessage()); } } mySocket.connect(new InetSocketAddress(target.getHost(), target.getPort()), connectTimeout); } else { callDebugInfo(DEBUG_SOCKET, "Using Proxy"); if (getBindIP() != null && !getBindIP().isEmpty()) { callDebugInfo(DEBUG_SOCKET, "IP Binding is not possible when using a proxy."); } final String proxyHost = proxy.getHost(); final int proxyPort = proxy.getPort(); if (proxyPort > 65535 || proxyPort <= 0) { throw new IOException("Proxy port (" + proxyPort + ") is invalid."); } final Proxy.Type proxyType = Proxy.Type.valueOf(proxy.getScheme().toUpperCase()); mySocket = new Socket(new Proxy(proxyType, new InetSocketAddress(proxyHost, proxyPort))); final IRCAuthenticator ia = IRCAuthenticator.getIRCAuthenticator(); try { try { ia.getSemaphore().acquire(); } catch (InterruptedException ex) { } ia.addAuthentication(target, proxy); mySocket.connect(new InetSocketAddress(target.getHost(), target.getPort()), connectTimeout); } finally { ia.removeAuthentication(target, proxy); ia.getSemaphore().release(); } } return mySocket; } /** * Find a socket to connect using, this will where possible attempt IPv6 * first, before falling back to IPv4. * * This will: * - Try using v6 first before v4. * - If there are any failures at all with v6 (including, not having a * v6 address...) then fall through to v4. * - If there is no v4 address, throw the v6 exception * - Otherwise, try v4 * - If there are failures with v4, throw the exception. * * If we have both IPv6 and IPv4, and a target host has both IPv6 and IPv4 * addresses, but does not listen on the requested port on either, this * will cause a double-length connection timeout. * * @param target Target URI to connect to. * @param proxy Proxy URI to use * @return Socket to use in future connections. * @throws IOException if there is an error. */ private Socket findSocket(final URI target, final URI proxy) throws IOException { URI target6 = null; URI target4 = null; // Get the v6 and v4 addresses if appropriate.. try { for (InetAddress i : InetAddress.getAllByName(target.getHost())) { if (target6 == null && i instanceof Inet6Address) { target6 = new URI(target.getScheme(), target.getUserInfo(), i.getHostAddress(), target.getPort(), target.getPath(), target.getQuery(), target.getFragment()); } else if (target4 == null && i instanceof Inet4Address) { target4 = new URI(target.getScheme(), target.getUserInfo(), i.getHostAddress(), target.getPort(), target.getPath(), target.getQuery(), target.getFragment()); } } } catch (final URISyntaxException use) { /* Won't happen. */ } // Now try and connect. // Try using v6 first before v4. // If there are any failures at all with v6 (including, not having a // v6 address...) then fall through to v4. // If there is no v4 address, throw the v6 exception // Otherwise, try v4 // If there are failures with v4, throw the exception. // // If we have both IPv6 and IPv4, and a target host has both IPv6 and // IPv4 addresses, but does not listen on the requested port on either, // this will cause a double-length connection timeout. // // In future this may want to be rewritten to perform a happy-eyeballs // race rather than trying one then the other. Exception v6Exception = null; if (target6 != null) { try { return newSocket(target6, proxy); } catch (final IOException ioe) { if (target4 == null) { throw ioe; } } catch (final Exception e) { v6Exception = e; callDebugInfo(DEBUG_SOCKET, "Exception trying to use IPv6: " + e); } } if (target4 != null) { try { return newSocket(target4, proxy); } catch (final IOException e2) { callDebugInfo(DEBUG_SOCKET, "Exception trying to use IPv4: " + e2); throw e2; } } else if (v6Exception != null) { throw new IOException("Error connecting to: " + target + " (IPv6 failure, with no IPv4 fallback)", v6Exception); } else { // We should never get here as if both target4 and target6 were null // getAllByName would have thrown an exception instead. throw new IOException("Error connecting to: " + target + " (General connectivity failure)"); } } /** * Connect to IRC. * * @throws IOException if the socket can not be connected * @throws NoSuchAlgorithmException if SSL is not available * @throws KeyManagementException if the trustManager is invalid */ private void doConnect() throws IOException, NoSuchAlgorithmException, KeyManagementException { if (getURI() == null || getURI().getHost() == null) { throw new UnknownHostException("Unspecified host."); } resetState(); callDebugInfo(DEBUG_SOCKET, "Connecting to " + getURI().getHost() + ":" + getURI().getPort()); currentSocketState = SocketState.OPENING; socket = findSocket(getConnectURI(getURI()), getProxy()); rawSocket = socket; if (getURI().getScheme().endsWith("s")) { callDebugInfo(DEBUG_SOCKET, "Server is SSL."); if (myTrustManager == null) { myTrustManager = trustAllCerts; } final SSLContext sc = SSLContext.getInstance("SSL"); sc.init(myKeyManagers, myTrustManager, new java.security.SecureRandom()); final SSLSocketFactory socketFactory = sc.getSocketFactory(); socket = socketFactory.createSocket(socket, getURI().getHost(), getURI().getPort(), false); // Manually start a handshake so we get proper SSL errors here, // and so that we can control the connection timeout final int timeout = socket.getSoTimeout(); socket.setSoTimeout(10000); ((SSLSocket) socket).startHandshake(); socket.setSoTimeout(timeout); currentSocketState = SocketState.OPENING; } callDebugInfo(DEBUG_SOCKET, "\t-> Opening socket output stream PrintWriter"); out.setOutputStream(socket.getOutputStream()); out.setQueueEnabled(true); currentSocketState = SocketState.OPEN; callDebugInfo(DEBUG_SOCKET, "\t-> Opening socket input stream BufferedReader"); in = new IRCReader(socket.getInputStream(), encoder); callDebugInfo(DEBUG_SOCKET, "\t-> Socket Opened"); } /** * Send server connection strings (NICK/USER/PASS). */ protected void sendConnectionStrings() { sendString("CAP LS"); if (getURI().getUserInfo() != null && !getURI().getUserInfo().isEmpty()) { sendString("PASS " + getURI().getUserInfo()); } sendString("NICK " + me.getNickname()); thinkNickname = me.getNickname(); String localhost; try { localhost = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException uhe) { localhost = "*"; } sendString("USER " + me.getUsername() + " " + localhost + " " + getURI().getHost() + " :" + me.getRealname()); } /** * Handle an onConnect error. * * @param e Exception to handle * @param isUserError Is this a user error? */ private void handleConnectException(final Exception e, final boolean isUserError) { callDebugInfo(DEBUG_SOCKET, "Error Connecting (" + e.getMessage() + "), Aborted"); final ParserError ei = new ParserError(ParserError.ERROR_ERROR + (isUserError ? ParserError.ERROR_USER : 0), "Exception with server socket", getLastLine()); ei.setException(e); callConnectError(ei); if (currentSocketState != SocketState.CLOSED) { currentSocketState = SocketState.CLOSED; callSocketClosed(); } resetState(); } /** * Begin execution. * Connect to server, and start parsing incoming lines */ @Override public void run() { callDebugInfo(DEBUG_INFO, "Begin Thread Execution"); if (hasBegan) { return; } else { hasBegan = true; } try { doConnect(); } catch (UnknownHostException e) { handleConnectException(e, true); return; } catch (IOException e) { handleConnectException(e, true); return; } catch (NoSuchAlgorithmException e) { handleConnectException(e, false); return; } catch (KeyManagementException e) { handleConnectException(e, false); return; } callDebugInfo(DEBUG_SOCKET, "Socket Connected"); sendConnectionStrings(); while (true) { try { lastLine = in.readLine(); // Blocking :/ if (lastLine == null) { if (currentSocketState != SocketState.CLOSED) { currentSocketState = SocketState.CLOSED; callSocketClosed(); } resetState(); break; } else if (currentSocketState != SocketState.CLOSING) { processLine(lastLine); } } catch (IOException e) { callDebugInfo(DEBUG_SOCKET, "Exception in main loop (" + e.getMessage() + "), Aborted"); if (currentSocketState != SocketState.CLOSED) { currentSocketState = SocketState.CLOSED; callSocketClosed(); } resetState(); break; } } callDebugInfo(DEBUG_INFO, "End Thread Execution"); } /** {@inheritDoc} */ @Override public int getLocalPort() { if (currentSocketState == SocketState.OPENING || currentSocketState == SocketState.OPEN) { return socket.getLocalPort(); } else { return 0; } } /** Close socket on destroy. */ @Override protected void finalize() throws Throwable { try { // See note at disconnect() method for why we close rawSocket. if (rawSocket != null) { rawSocket.close(); } } catch (IOException e) { callDebugInfo(DEBUG_SOCKET, "Could not close socket"); } super.finalize(); } /** * Get the trailing parameter for a line. * The parameter is everything after the first occurance of " :" ot the last token in the line after a space. * * @param line Line to get parameter for * @return Parameter of the line */ public static String getParam(final String line) { String[] params = null; params = line.split(" :", 2); return params[params.length - 1]; } /** * Tokenise a line. * splits by " " up to the first " :" everything after this is a single token * * @param line Line to tokenise * @return Array of tokens */ public static String[] tokeniseLine(final String line) { if (line == null) { return new String[]{""}; // Return empty string[] } final int lastarg = line.indexOf(" :"); String[] tokens; if (lastarg > -1) { final String[] temp = line.substring(0, lastarg).split(" "); tokens = new String[temp.length + 1]; System.arraycopy(temp, 0, tokens, 0, temp.length); tokens[temp.length] = line.substring(lastarg + 2); } else { tokens = line.split(" "); } if (tokens.length < 1) { tokens = new String[]{""}; } return tokens; } /** {@inheritDoc} */ @Override public IRCClientInfo getClient(final String details) { final String sWho = getStringConverter().toLowerCase(IRCClientInfo.parseHost(details)); if (clientList.containsKey(sWho)) { return clientList.get(sWho); } else { return new IRCClientInfo(this, details).setFake(true); } } public boolean isKnownClient(final String host) { final String sWho = getStringConverter().toLowerCase(IRCClientInfo.parseHost(host)); return clientList.containsKey(sWho); } /** {@inheritDoc} */ @Override public IRCChannelInfo getChannel(final String channel) { synchronized (channelList) { return channelList.get(getStringConverter().toLowerCase(channel)); } } /** {@inheritDoc} */ @Override public void sendInvite(final String channel, final String user) { sendRawMessage("INVITE " + user + " " + channel); } /** {@inheritDoc} */ @Override public void sendRawMessage(final String message) { doSendString(message, QueuePriority.NORMAL, false); } /** {@inheritDoc} */ @Override public void sendRawMessage(final String message, final QueuePriority priority) { doSendString(message, priority, false); } /** * Send a line to the server and add proper line ending. * * @param line Line to send (\r\n termination is added automatically) * @return True if line was sent, else false. */ protected boolean sendString(final String line) { return doSendString(line, QueuePriority.NORMAL, true); } /** * Send a line to the server and add proper line ending. * If a non-empty argument is given, it is appended as a trailing argument * (i.e., separated by " :"); otherwise, the line is sent as-is. * * @param line Line to send * @param argument Trailing argument for the command, if any * @return True if line was sent, else false. */ protected boolean sendString(final String line, final String argument) { return sendString(argument.isEmpty() ? line : line + " :" + argument); } /** * Send a line to the server and add proper line ending. * * @param line Line to send (\r\n termination is added automatically) * @param priority Priority of this line. * @return True if line was sent, else false. */ protected boolean sendString(final String line, final QueuePriority priority) { return doSendString(line, priority, true); } /** * Send a line to the server and add proper line ending. * * @param line Line to send (\r\n termination is added automatically) * @param priority Priority of this line. * @param fromParser is this line from the parser? (used for callDataOut) * @return True if line was sent, else false. */ protected boolean doSendString(final String line, final QueuePriority priority, final boolean fromParser) { if (out == null || getSocketState() != SocketState.OPEN) { return false; } callDataOut(line, fromParser); out.sendLine(line, priority); final String[] newLine = tokeniseLine(line); if (newLine[0].equalsIgnoreCase("away") && newLine.length > 1) { myself.setAwayReason(newLine[newLine.length - 1]); } else if (newLine[0].equalsIgnoreCase("mode") && newLine.length == 3) { final IRCChannelInfo channel = getChannel(newLine[1]); if (channel != null) { // This makes sure we don't add the same item to the LMQ twice, // even if its requested twice, as the ircd will only reply once final Deque foundModes = new LinkedList(); final Queue listModeQueue = channel.getListModeQueue(); for (int i = 0; i < newLine[2].length(); ++i) { final Character mode = newLine[2].charAt(i); callDebugInfo(DEBUG_LMQ, "Intercepted mode request for " + channel + " for mode " + mode); if (chanModesOther.containsKey(mode) && chanModesOther.get(mode) == MODE_LIST) { if (foundModes.contains(mode)) { callDebugInfo(DEBUG_LMQ, "Already added to LMQ"); } else { listModeQueue.offer(mode); foundModes.offer(mode); callDebugInfo(DEBUG_LMQ, "Added to LMQ"); } } } } } return true; } /** {@inheritDoc} */ @Override public String getNetworkName() { return networkName; } /** {@inheritDoc} */ @Override public String getLastLine() { return lastLine == null ? "" : lastLine.getLine(); } /** {@inheritDoc} */ @Override public List getServerInformationLines() { synchronized (serverInformationLines) { return new LinkedList(serverInformationLines); } } /** * Process a line and call relevant methods for handling. * * @param line Line read from the IRC server */ @SuppressWarnings("fallthrough") protected void processLine(final ReadLine line) { callDataIn(line.getLine()); String[] token = line.getTokens(); Date lineTS = new Date(); if (!token[0].isEmpty() && token[0].charAt(0) == '@') { try { final int tsEnd = token[0].indexOf('@', 1); final long ts = Long.parseLong(token[0].substring(1, tsEnd)); token[0] = token[0].substring(tsEnd + 1); lineTS = new Date(ts - tsdiff); } catch (final NumberFormatException nfe) { /* Do nothing. */ } } int nParam; setPingNeeded(false); if (token.length < 2) { return; } try { final String sParam = token[1]; if (token[0].equalsIgnoreCase("PING") || token[1].equalsIgnoreCase("PING")) { sendString("PONG :" + sParam, QueuePriority.HIGH); } else if (token[0].equalsIgnoreCase("PONG") || token[1].equalsIgnoreCase("PONG")) { if (!lastPingValue.isEmpty() && lastPingValue.equals(token[token.length - 1])) { lastPingValue = ""; serverLag = System.currentTimeMillis() - pingTime; callPingSuccess(); } } else if (token[0].equalsIgnoreCase("ERROR")) { final StringBuilder errorMessage = new StringBuilder(); for (int i = 1; i < token.length; ++i) { errorMessage.append(token[i]); } callServerError(errorMessage.toString()); } else if (token[1].equalsIgnoreCase("TSIRC") && token.length > 3) { if (token[2].equals("1")) { try { final long ts = Long.parseLong(token[3]); tsdiff = ts - System.currentTimeMillis(); } catch (final NumberFormatException nfe) { /* Do nothing. */ } } } else { if (got001) { // Freenode sends a random notice in a stupid place, others might do aswell // These shouldn't cause post005 to be fired, so handle them here. if (token[0].equalsIgnoreCase("NOTICE") || (token.length > 2 && token[2].equalsIgnoreCase("NOTICE"))) { try { myProcessingManager.process(lineTS, "Notice Auth", token); } catch (ProcessorNotFoundException e) { // ??? } return; } if (!post005) { try { nParam = Integer.parseInt(token[1]); } catch (NumberFormatException e) { nParam = -1; } if (nParam < 0 || nParam > 5) { callPost005(); } else { // Store 001 - 005 for informational purposes. synchronized (serverInformationLines) { serverInformationLines.add(line.getLine()); } } } // After 001 we potentially care about everything! try { myProcessingManager.process(lineTS, sParam, token); } catch (ProcessorNotFoundException e) { // ??? } } else { // Before 001 we don't care about much. try { nParam = Integer.parseInt(token[1]); } catch (NumberFormatException e) { nParam = -1; } switch (nParam) { case 1: // 001 - Welcome to IRC synchronized (serverInformationLines) { serverInformationLines.add(line.getLine()); } // Fallthrough case 464: // Password Required case 433: // Nick In Use try { myProcessingManager.process(sParam, token); } catch (ProcessorNotFoundException e) { } break; default: // Unknown - Send to Notice Auth // Some networks send a CTCP during the auth process, handle it if (token.length > 3 && !token[3].isEmpty() && token[3].charAt(0) == (char) 1 && token[3].charAt(token[3].length() - 1) == (char) 1) { try { myProcessingManager.process(lineTS, sParam, token); } catch (ProcessorNotFoundException e) { } break; } // Some networks may send a NICK message if you nick change before 001 // Eat it up so that it isn't treated as a notice auth. if (token[1].equalsIgnoreCase("NICK")) { break; } // CAP also happens here, so try that. if (token[1].equalsIgnoreCase("CAP")) { myProcessingManager.process(lineTS, sParam, token); } // Otherwise, send to Notice Auth try { myProcessingManager.process(lineTS, "Notice Auth", token); } catch (ProcessorNotFoundException e) { } break; } } } } catch (Exception e) { final ParserError ei = new ParserError(ParserError.ERROR_FATAL, "Fatal Exception in Parser.", getLastLine()); ei.setException(e); callErrorInfo(ei); } } /** The IRCStringConverter for this parser */ private IRCStringConverter stringConverter = null; /** {@inheritDoc} */ @Override public IRCStringConverter getStringConverter() { if (stringConverter == null) { stringConverter = new IRCStringConverter(IRCEncoding.RFC1459); } return stringConverter; } /** * Sets the encoding that this parser's string converter should use. * * @param encoding The encoding to use */ protected void setEncoding(final IRCEncoding encoding) { stringConverter = new IRCStringConverter(encoding); } /** * Check the state of the requested capability. * * @return State of the requested capability. */ public CapabilityState getCapabilityState(final String capability) { synchronized (capabilities) { if (capabilities.containsKey(capability.toLowerCase())) { return capabilities.get(capability.toLowerCase()); } else { return CapabilityState.INVALID; } } } /** * Set the state of the requested capability. * * @param capability Requested capability * @param state State to set for capability */ public void setCapabilityState(final String capability, final CapabilityState state) { synchronized (capabilities) { if (capabilities.containsKey(capability.toLowerCase())) { capabilities.put(capability.toLowerCase(), state); } } } /** * Add the given capability as a supported capability by the server. * * @param capability Requested capability */ public void addCapability(final String capability) { synchronized (capabilities) { capabilities.put(capability.toLowerCase(), CapabilityState.DISABLED); } } /** * Get the server capabilities and their current state. * * @return Server capabilities and their current state. */ public Map getCapabilities() { synchronized (capabilities) { return new HashMap(capabilities); } } /** * Process CHANMODES from 005. */ public void parseChanModes() { final StringBuilder sDefaultModes = new StringBuilder("b,k,l,"); String[] bits = null; String modeStr; if (h005Info.containsKey("USERCHANMODES")) { if (getServerType() == ServerType.DANCER) { sDefaultModes.insert(0, "dqeI"); } else if (getServerType() == ServerType.AUSTIRC) { sDefaultModes.insert(0, "e"); } modeStr = h005Info.get("USERCHANMODES"); char mode; for (int i = 0; i < modeStr.length(); ++i) { mode = modeStr.charAt(i); if (!prefixModes.containsKey(mode) && sDefaultModes.indexOf(Character.toString(mode)) < 0) { sDefaultModes.append(mode); } } } else { sDefaultModes.append("imnpstrc"); } if (h005Info.containsKey("CHANMODES")) { modeStr = h005Info.get("CHANMODES"); } else { modeStr = sDefaultModes.toString(); h005Info.put("CHANMODES", modeStr); } bits = modeStr.split(",", 5); if (bits.length < 4) { modeStr = sDefaultModes.toString(); callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "CHANMODES String not valid. Using default string of \"" + modeStr + "\"", getLastLine())); h005Info.put("CHANMODES", modeStr); bits = modeStr.split(",", 5); } // resetState chanModesOther.clear(); chanModesBool.clear(); nextKeyCMBool = 1; // List modes. for (int i = 0; i < bits[0].length(); ++i) { final Character cMode = bits[0].charAt(i); callDebugInfo(DEBUG_INFO, "Found List Mode: %c", cMode); if (!chanModesOther.containsKey(cMode)) { chanModesOther.put(cMode, MODE_LIST); } } // Param for Set and Unset. final Byte nBoth = MODE_SET + MODE_UNSET; for (int i = 0; i < bits[1].length(); ++i) { final Character cMode = bits[1].charAt(i); callDebugInfo(DEBUG_INFO, "Found Set/Unset Mode: %c", cMode); if (!chanModesOther.containsKey(cMode)) { chanModesOther.put(cMode, nBoth); } } // Param just for Set for (int i = 0; i < bits[2].length(); ++i) { final Character cMode = bits[2].charAt(i); callDebugInfo(DEBUG_INFO, "Found Set Only Mode: %c", cMode); if (!chanModesOther.containsKey(cMode)) { chanModesOther.put(cMode, MODE_SET); } } // Boolean Mode for (int i = 0; i < bits[3].length(); ++i) { final Character cMode = bits[3].charAt(i); callDebugInfo(DEBUG_INFO, "Found Boolean Mode: %c [%d]", cMode, nextKeyCMBool); if (!chanModesBool.containsKey(cMode)) { chanModesBool.put(cMode, nextKeyCMBool); nextKeyCMBool *= 2; } } } /** {@inheritDoc} */ @Override public String getChannelUserModes() { if (h005Info.containsKey("PREFIXSTRING")) { return h005Info.get("PREFIXSTRING"); } else { return ""; } } /** {@inheritDoc} */ @Override public String getBooleanChannelModes() { final char[] modes = new char[chanModesBool.size()]; int i = 0; for (char mode : chanModesBool.keySet()) { modes[i++] = mode; } // Alphabetically sort the array Arrays.sort(modes); return new String(modes); } /** {@inheritDoc} */ @Override public String getListChannelModes() { return getOtherModeString(MODE_LIST); } /** {@inheritDoc} */ @Override public String getParameterChannelModes() { return getOtherModeString(MODE_SET); } /** {@inheritDoc} */ @Override public String getDoubleParameterChannelModes() { return getOtherModeString((byte) (MODE_SET + MODE_UNSET)); } /** {@inheritDoc} */ @Override public String getChannelPrefixes() { if (chanPrefix.isEmpty()) { return "#&"; } final StringBuilder builder = new StringBuilder(chanPrefix.size()); synchronized (chanPrefix) { for (Character prefix : chanPrefix) { builder.append(prefix); } } return builder.toString(); } /** * Get modes from hChanModesOther that have a specific value. * Modes are returned in alphabetical order * * @param value Value mode must have to be included * @return All the currently known Set-Unset modes */ protected String getOtherModeString(final byte value) { final char[] modes = new char[chanModesOther.size()]; Byte nTemp; int i = 0; for (char cTemp : chanModesOther.keySet()) { nTemp = chanModesOther.get(cTemp); if (nTemp == value) { modes[i++] = cTemp; } } // Alphabetically sort the array Arrays.sort(modes); return new String(modes).trim(); } /** {@inheritDoc} */ @Override public String getUserModes() { if (h005Info.containsKey("USERMODES")) { return h005Info.get("USERMODES"); } else { return ""; } } /** * Process USERMODES from 004. */ protected void parseUserModes() { final String sDefaultModes = "nwdoi"; String modeStr; if (h005Info.containsKey("USERMODES")) { modeStr = h005Info.get("USERMODES"); } else { modeStr = sDefaultModes; h005Info.put("USERMODES", sDefaultModes); } // resetState userModes.clear(); nNextKeyUser = 1; // Boolean Mode for (int i = 0; i < modeStr.length(); ++i) { final Character cMode = modeStr.charAt(i); callDebugInfo(DEBUG_INFO, "Found User Mode: %c [%d]", cMode, nNextKeyUser); if (!userModes.containsKey(cMode)) { userModes.put(cMode, nNextKeyUser); nNextKeyUser *= 2; } } } /** * Process CHANTYPES from 005. */ protected void parseChanPrefix() { final String sDefaultModes = "#&"; String modeStr; if (h005Info.containsKey("CHANTYPES")) { modeStr = h005Info.get("CHANTYPES"); } else { modeStr = sDefaultModes; h005Info.put("CHANTYPES", sDefaultModes); } // resetState chanPrefix.clear(); // Boolean Mode for (int i = 0; i < modeStr.length(); ++i) { final Character cMode = modeStr.charAt(i); callDebugInfo(DEBUG_INFO, "Found Chan Prefix: %c", cMode); if (!chanPrefix.contains(cMode)) { chanPrefix.add(cMode); } } } /** * Process PREFIX from 005. */ public void parsePrefixModes() { final String sDefaultModes = "(ohv)@%+"; String[] bits; String modeStr; if (h005Info.containsKey("PREFIX")) { modeStr = h005Info.get("PREFIX"); } else { modeStr = sDefaultModes; } if (modeStr.substring(0, 1).equals("(")) { modeStr = modeStr.substring(1); } else { modeStr = sDefaultModes.substring(1); h005Info.put("PREFIX", sDefaultModes); } bits = modeStr.split("\\)", 2); if (bits.length != 2 || bits[0].length() != bits[1].length()) { modeStr = sDefaultModes; callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "PREFIX String not valid. Using default string of \"" + modeStr + "\"", getLastLine())); h005Info.put("PREFIX", modeStr); modeStr = modeStr.substring(1); bits = modeStr.split("\\)", 2); } // resetState prefixModes.clear(); prefixMap.clear(); nextKeyPrefix = 1; for (int i = bits[0].length() - 1; i > -1; --i) { final Character cMode = bits[0].charAt(i); final Character cPrefix = bits[1].charAt(i); callDebugInfo(DEBUG_INFO, "Found Prefix Mode: %c => %c [%d]", cMode, cPrefix, nextKeyPrefix); if (!prefixModes.containsKey(cMode)) { prefixModes.put(cMode, nextKeyPrefix); prefixMap.put(cMode, cPrefix); prefixMap.put(cPrefix, cMode); nextKeyPrefix *= 2; } } h005Info.put("PREFIXSTRING", bits[0]); } /** {@inheritDoc} */ @Override public void joinChannels(final ChannelJoinRequest... channels) { // We store a map from key->channels to allow intelligent joining of // channels using as few JOIN commands as needed. final Map joinMap = new HashMap(); for (ChannelJoinRequest channel : channels) { // Make sure we have a list to put stuff in. StringBuffer list = joinMap.get(channel.getPassword()); if (list == null) { list = new StringBuffer(); joinMap.put(channel.getPassword(), list); } // Add the channel to the list. If the name is invalid and // autoprefix is off we will just skip this channel. if (!channel.getName().isEmpty()) { if (list.length() > 0) { list.append(','); } if (!isValidChannelName(channel.getName())) { if (h005Info.containsKey("CHANTYPES")) { final String chantypes = h005Info.get("CHANTYPES"); if (chantypes.isEmpty()) { list.append('#'); } else { list.append(chantypes.charAt(0)); } } else { list.append('#'); } } list.append(channel.getName()); } } for (Map.Entry entrySet : joinMap.entrySet()) { final String thisKey = entrySet.getKey(); final String channelString = entrySet.getValue().toString(); if (!channelString.isEmpty()) { if (thisKey == null || thisKey.isEmpty()) { sendString("JOIN " + channelString); } else { sendString("JOIN " + channelString + " " + thisKey); } } } } /** * Leave a Channel. * * @param channel Name of channel to part * @param reason Reason for leaving (Nothing sent if sReason is "") */ public void partChannel(final String channel, final String reason) { if (getChannel(channel) == null) { return; } sendString("PART " + channel, reason); } /** * Set Nickname. * * @param nickname New nickname wanted. */ public void setNickname(final String nickname) { if (getSocketState() == SocketState.OPEN) { if (!myself.isFake() && myself.getRealNickname().equals(nickname)) { return; } sendString("NICK " + nickname); } else { me.setNickname(nickname); } thinkNickname = nickname; } /** {@inheritDoc} */ @Override public int getMaxLength(final String type, final String target) { // If my host is "nick!user@host" and we are sending "#Channel" // a "PRIVMSG" this will find the length of ":nick!user@host PRIVMSG #channel :" // and subtract it from the MAX_LINELENGTH. This should be sufficient in most cases. // Lint = the 2 ":" at the start and end and the 3 separating " "s int length = 0; if (type != null) { length += type.length(); } if (target != null) { length += target.length(); } return getMaxLength(length); } /** * Get the max length a message can be. * * @param length Length of stuff. (Ie "PRIVMSG"+"#Channel") * @return Max Length message should be. */ public int getMaxLength(final int length) { final int lineLint = 5; if (myself.isFake()) { callErrorInfo(new ParserError(ParserError.ERROR_ERROR + ParserError.ERROR_USER, "getMaxLength() called, but I don't know who I am?", getLastLine())); return MAX_LINELENGTH - length - lineLint; } else { return MAX_LINELENGTH - myself.toString().length() - length - lineLint; } } /** {@inheritDoc} */ @Override public int getMaxListModes(final char mode) { // MAXLIST=bdeI:50 // MAXLIST=b:60,e:60,I:60 // MAXBANS=30 int result = -2; callDebugInfo(DEBUG_INFO, "Looking for maxlistmodes for: " + mode); // Try in MAXLIST if (h005Info.get("MAXLIST") != null) { if (h005Info.get("MAXBANS") == null) { result = 0; } final String maxlist = h005Info.get("MAXLIST"); callDebugInfo(DEBUG_INFO, "Found maxlist (" + maxlist + ")"); final String[] bits = maxlist.split(","); for (String bit : bits) { final String[] parts = bit.split(":", 2); callDebugInfo(DEBUG_INFO, "Bit: " + bit + " | parts.length = " + parts.length + " (" + parts[0] + " -> " + parts[0].indexOf(mode) + ")"); if (parts.length == 2 && parts[0].indexOf(mode) > -1) { callDebugInfo(DEBUG_INFO, "parts[0] = '" + parts[0] + "' | parts[1] = '" + parts[1] + "'"); try { result = Integer.parseInt(parts[1]); break; } catch (NumberFormatException nfe) { result = -1; } } } } // If not in max list, try MAXBANS if (result == -2 && h005Info.get("MAXBANS") != null) { callDebugInfo(DEBUG_INFO, "Trying max bans"); try { result = Integer.parseInt(h005Info.get("MAXBANS")); } catch (NumberFormatException nfe) { result = -1; } } else if (result == -2 && getServerType() == ServerType.WEIRCD) { result = 50; } else if (result == -2 && getServerType() == ServerType.OTHERNET) { result = 30; } else if (result == -2) { result = -1; callDebugInfo(DEBUG_INFO, "Failed"); callErrorInfo(new ParserError(ParserError.ERROR_ERROR + ParserError.ERROR_USER, "Unable to discover max list modes.", getLastLine())); } callDebugInfo(DEBUG_INFO, "Result: " + result); return result; } /** {@inheritDoc} */ @Override public void sendMessage(final String target, final String message) { if (target == null || message == null) { return; } if (target.isEmpty()) { return; } sendString("PRIVMSG " + target, message); } /** {@inheritDoc} */ @Override public void sendNotice(final String target, final String message) { if (target == null || message == null) { return; } if (target.isEmpty()) { return; } sendString("NOTICE " + target, message); } /** {@inheritDoc} */ @Override public void sendAction(final String target, final String message) { sendCTCP(target, "ACTION", message); } /** {@inheritDoc} */ @Override public void sendCTCP(final String target, final String type, final String message) { if (target == null || message == null) { return; } if (target.isEmpty() || type.isEmpty()) { return; } final char char1 = (char) 1; sendString("PRIVMSG " + target, char1 + type.toUpperCase() + " " + message + char1); } /** {@inheritDoc} */ @Override public void sendCTCPReply(final String target, final String type, final String message) { if (target == null || message == null) { return; } if (target.isEmpty() || type.isEmpty()) { return; } final char char1 = (char) 1; sendString("NOTICE " + target, char1 + type.toUpperCase() + " " + message + char1); } /** {@inheritDoc} */ @Override public void requestGroupList(final String searchTerms) { sendString("LIST", searchTerms); } /** {@inheritDoc} */ @Override public void quit(final String reason) { sendString("QUIT", reason); } /** {@inheritDoc} */ @Override public void disconnect(final String message) { super.disconnect(message); if (currentSocketState == SocketState.OPENING || currentSocketState == SocketState.OPEN) { currentSocketState = SocketState.CLOSING; if (got001) { quit(message); } } try { // SSLSockets try to close nicely and read data from the socket, // which seems to hang indefinitely in some circumstances. We don't // like indefinite hangs, so just close the underlying socket // direct. if (rawSocket != null) { rawSocket.close(); } } catch (IOException e) { /* Do Nothing */ } finally { if (currentSocketState != SocketState.CLOSED) { currentSocketState = SocketState.CLOSED; callSocketClosed(); } resetState(); } } /** {@inheritDoc} * * - Before channel prefixes are known (005/noMOTD/MOTDEnd), this checks * that the first character is either #, &, ! or + * - Assumes that any channel that is already known is valid, even if * 005 disagrees. */ @Override public boolean isValidChannelName(final String name) { // Check sChannelName is not empty or null if (name == null || name.isEmpty()) { return false; } // Check its not ourself (PM recieved before 005) if (getStringConverter().equalsIgnoreCase(getMyNickname(), name)) { return false; } // Check if we are already on this channel if (getChannel(name) != null) { return true; } // Check if we know of any valid chan prefixes if (chanPrefix.isEmpty()) { // We don't. Lets check against RFC2811-Specified channel types final char first = name.charAt(0); return first == '#' || first == '&' || first == '!' || first == '+'; } // Otherwise return true if: // Channel equals "0" // first character of the channel name is a valid channel prefix. return chanPrefix.contains(name.charAt(0)) || "0".equals(name); } /** {@inheritDoc} */ @Override public boolean isUserSettable(final char mode) { String validmodes; if (h005Info.containsKey("USERCHANMODES")) { validmodes = h005Info.get("USERCHANMODES"); } else { validmodes = "bklimnpstrc"; } return validmodes.matches(".*" + mode + ".*"); } /** * Get the 005 info. * * @return 005Info hashtable. */ public Map get005() { return h005Info; } /** * Get the ServerType for this IRCD. * * @return The ServerType for this IRCD. */ public ServerType getServerType() { return ServerType.findServerType(h005Info.get("004IRCD"), networkName, h005Info.get("003IRCD"), h005Info.get("002IRCD")); } /** {@inheritDoc} */ @Override public String getServerSoftware() { final String version = h005Info.get("004IRCD"); return version == null ? "" : version; } /** {@inheritDoc} */ @Override public String getServerSoftwareType() { return getServerType().getType(); } /** * Get the value of checkServerPing. * * @return value of checkServerPing. * @see setCheckServerPing */ public boolean getCheckServerPing() { return checkServerPing; } /** * Set the value of checkServerPing. * * @param newValue New value to use. * @see setCheckServerPing */ public void setCheckServerPing(final boolean newValue) { checkServerPing = newValue; if (checkServerPing) { startPingTimer(); } else { stopPingTimer(); } } /** {@inheritDoc} */ @Override public void setEncoder(final Encoder encoder) { this.encoder = encoder; } /** {@inheritDoc} */ @Override public void setPingTimerInterval(final long newValue) { super.setPingTimerInterval(newValue); startPingTimer(); } /** * Start the pingTimer. */ protected void startPingTimer() { pingTimerSem.acquireUninterruptibly(); try { setPingNeeded(false); if (pingTimer != null) { pingTimer.cancel(); } pingTimer = new Timer("IRCParser pingTimer"); pingTimer.schedule(new PingTimer(this, pingTimer), 0, getPingTimerInterval()); pingCountDown = 1; } finally { pingTimerSem.release(); } } /** * Stop the pingTimer. */ protected void stopPingTimer() { pingTimerSem.acquireUninterruptibly(); if (pingTimer != null) { pingTimer.cancel(); pingTimer = null; } pingTimerSem.release(); } /** * This is called when the ping Timer has been executed. * As the timer is restarted on every incomming message, this will only be * called when there has been no incomming line for 10 seconds. * * @param timer The timer that called this. */ protected void pingTimerTask(final Timer timer) { // If user no longer wants server ping to be checked, or the socket is // closed then cancel the time and do nothing else. if (!getCheckServerPing() || getSocketState() != SocketState.OPEN) { pingTimerSem.acquireUninterruptibly(); if (pingTimer != null && pingTimer.equals(timer)) { pingTimer.cancel(); } pingTimerSem.release(); return; } if (getPingNeeded()) { if (!callPingFailed()) { pingTimerSem.acquireUninterruptibly(); if (pingTimer != null && pingTimer.equals(timer)) { pingTimer.cancel(); } pingTimerSem.release(); disconnect("Server not responding."); } } else { --pingCountDown; if (pingCountDown < 1) { pingTime = System.currentTimeMillis(); setPingNeeded(true); pingCountDown = getPingTimerFraction(); lastPingValue = String.valueOf(System.currentTimeMillis()); if (sendString("PING " + lastPingValue, QueuePriority.HIGH)) { callPingSent(); } } } } /** {@inheritDoc} */ @Override public long getServerLatency() { return serverLag; } /** * Updates the name of the server that this parser is connected to. * * @param serverName The discovered server name */ protected void updateServerName(final String serverName) { setServerName(serverName); } /** * Get the current server lag. * * @param actualTime if True the value returned will be the actual time the ping was sent * else it will be the amount of time sinse the last ping was sent. * @return Time last ping was sent */ public long getPingTime(final boolean actualTime) { if (actualTime) { return pingTime; } else { return System.currentTimeMillis() - pingTime; } } /** {@inheritDoc} */ @Override public long getPingTime() { return getPingTime(false); } /** * Set if a ping is needed or not. * * @param newStatus new value to set pingNeeded to. */ private void setPingNeeded(final boolean newStatus) { pingNeeded.set(newStatus); } /** * Get if a ping is needed or not. * * @return value of pingNeeded. */ private boolean getPingNeeded() { return pingNeeded.get(); } /** {@inheritDoc} */ @Override public IRCClientInfo getLocalClient() { return myself; } /** * Get the current nickname. * After 001 this returns the exact same as getLocalClient().getRealNickname(); * Before 001 it returns the nickname that the parser Thinks it has. * * @return Current nickname. */ public String getMyNickname() { if (myself.isFake()) { return thinkNickname; } else { return myself.getRealNickname(); } } /** * Retrieves the local user information that this parser was configured * with. * * @return This parser's local user configuration */ public MyInfo getMyInfo() { return me; } /** * Get the current username (Specified in MyInfo on construction). * Get the username given in MyInfo * * @return My username. */ public String getMyUsername() { return me.getUsername(); } /** * Add a client to the ClientList. * * @param client Client to add */ public void addClient(final IRCClientInfo client) { clientList.put(getStringConverter().toLowerCase(client.getRealNickname()), client); } /** * Remove a client from the ClientList. * This WILL NOT allow cMyself to be removed from the list. * * @param client Client to remove */ public void removeClient(final IRCClientInfo client) { if (client != myself) { forceRemoveClient(client); } } /** * Remove a client from the ClientList. * This WILL allow cMyself to be removed from the list * * @param client Client to remove */ protected void forceRemoveClient(final IRCClientInfo client) { clientList.remove(getStringConverter().toLowerCase(client.getRealNickname())); } /** * Get the number of known clients. * * @return Count of known clients */ public int knownClients() { return clientList.size(); } /** * Get the known clients as a collection. * * @return Known clients as a collection */ public Collection getClients() { return clientList.values(); } /** * Clear the client list. */ public void clearClients() { clientList.clear(); addClient(getLocalClient()); } /** * Add a channel to the ChannelList. * * @param channel Channel to add */ public void addChannel(final IRCChannelInfo channel) { synchronized (channelList) { channelList.put(getStringConverter().toLowerCase(channel.getName()), channel); } } /** * Remove a channel from the ChannelList. * * @param channel Channel to remove */ public void removeChannel(final IRCChannelInfo channel) { synchronized (channelList) { channelList.remove(getStringConverter().toLowerCase(channel.getName())); } } /** * Get the number of known channel. * * @return Count of known channel */ public int knownChannels() { synchronized (channelList) { return channelList.size(); } } /** {@inheritDoc} */ @Override public Collection getChannels() { synchronized (channelList) { return channelList.values(); } } /** * Clear the channel list. */ public void clearChannels() { synchronized (channelList) { channelList.clear(); } } /** {@inheritDoc} */ @Override public String[] parseHostmask(final String hostmask) { return IRCClientInfo.parseHostFull(hostmask); } /** {@inheritDoc} */ @Override public int getMaxTopicLength() { if (h005Info.containsKey("TOPICLEN")) { try { return Integer.parseInt(h005Info.get("TOPICLEN")); } catch (NumberFormatException ex) { // Do nothing } } return 0; } /** {@inheritDoc} */ @Override public int getMaxLength() { return MAX_LINELENGTH; } /** {@inheritDoc} */ @Override public void setCompositionState(final String host, final CompositionState state) { // Do nothing } }