/* * Copyright (c) 2006-2009 Chris Smith, Shane Mc Cormack, Gregory Holmes * * 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.irc.outputqueue.OutputQueue; import com.dmdirc.parser.common.MyInfo; import com.dmdirc.parser.common.IgnoreList; import com.dmdirc.parser.common.ParserError; import com.dmdirc.parser.interfaces.SecureParser; import com.dmdirc.parser.interfaces.callbacks.ConnectErrorListener; import com.dmdirc.parser.interfaces.callbacks.DataInListener; import com.dmdirc.parser.interfaces.callbacks.DataOutListener; import com.dmdirc.parser.interfaces.callbacks.DebugInfoListener; import com.dmdirc.parser.interfaces.callbacks.ErrorInfoListener; import com.dmdirc.parser.interfaces.callbacks.PingFailureListener; import com.dmdirc.parser.interfaces.callbacks.PingSentListener; import com.dmdirc.parser.interfaces.callbacks.PingSuccessListener; import com.dmdirc.parser.interfaces.callbacks.Post005Listener; import com.dmdirc.parser.interfaces.callbacks.ServerErrorListener; import com.dmdirc.parser.interfaces.callbacks.SocketCloseListener; import com.dmdirc.parser.common.CallbackManager; import com.dmdirc.parser.common.QueuePriority; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; import java.net.URI; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collection; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.Queue; 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.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** * IRC Parser. * * @author Shane Mc Cormack */ public class IRCParser implements SecureParser, Runnable { /** 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; // public static final int DEBUG_SOMETHING = 16; //Next thingy /** 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. */ public MyInfo me = new MyInfo(); /** Server Info requested by user. */ public ServerInfo server = new ServerInfo(); /** 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 Semaphore pingTimerSem = new Semaphore(1); /** Length of time to wait between ping stuff. */ private long pingTimerLength = 10000; /** Is a ping needed? */ private volatile 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; /** * Amount of times the timer has to fire for inactivity before sending a ping. * * @see setPingCountDownLength */ private int pingCountDownLength = 6; /** Name the server calls itself. */ String serverName; /** 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 recieved the 001. */ boolean got001; /** Have we fired post005? */ boolean post005; /** Has the thread started execution yet, (Prevents run() being called multiple times). */ boolean hasBegan; /** Hashtable storing known prefix modes (ohv). */ final Map prefixModes = new Hashtable(); /** * 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 Hashtable(); /** 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 Hashtable(); /** 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 Hashtable(); /** 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 Hashtable(); /** The last line of input recieved from the server */ String lastLine = ""; /** Should the lastline (where given) be appended to the "data" part of any onErrorInfo call? */ boolean addLastLine = false; /** * Channel Prefixes (ie # + etc). * The "value" for these is always true. */ final Map chanPrefix = new Hashtable(); /** Hashtable storing all known clients based on nickname (in lowercase). */ private final Map clientList = new Hashtable(); /** Hashtable storing all known channels based on chanel name (inc prefix - in lowercase). */ private final Map channelList = new Hashtable(); /** 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 Hashtable(); /** Ignore List. */ IgnoreList myIgnoreList = new IgnoreList(); /** Reference to the callback Manager. */ CallbackManager myCallbackManager = new IRCCallbackManager(this); /** Reference to the Processing Manager. */ 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; /** Used for writing to the server. */ private OutputQueue out; /** Used for reading from the server. */ private BufferedReader 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 fake (channel)clients be created for callbacks where they do not exist? */ boolean createFake = true; /** Should channels automatically request list modes? */ boolean autoListMode = true; /** Should part/quit/kick callbacks be fired before removing the user internally? */ 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 the IP we want to bind to. */ private String bindIP = ""; /** This is list containing 001 - 005 inclusive. */ private final List serverInformationLines = new LinkedList(); /** * 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 serverDetails Server information. */ public IRCParser(final ServerInfo serverDetails) { this(null, serverDetails); } /** * Constructor with MyInfo, ServerInfo needs to be added separately (using IRC.server). * * @param myDetails Client information. */ public IRCParser(final MyInfo myDetails) { this(myDetails, (ServerInfo) null); } /** * Constructor with ServerInfo and MyInfo. * * @param serverDetails Server information. * @param myDetails Client information. */ public IRCParser(final MyInfo myDetails, final ServerInfo serverDetails) { out = new OutputQueue(); if (myDetails != null) { this.me = myDetails; } if (serverDetails != null) { this.server = serverDetails; } resetState(); } /** * Creates a new IRCParser with the specified client details which will * connect to the specified URI. * * @since 0.6.4 * @param myDetails The client details to use * @param uri The URI to connect to */ public IRCParser(final MyInfo myDetails, final URI uri) { this(myDetails, new ServerInfo(uri)); } /** * Get the current OutputQueue * * @return the current OutputQueue */ public OutputQueue getOutputQueue() { return out; } /** * Get the current Value of bindIP. * * @return Value of bindIP ("" for default IP) */ public String getBindIP() { return bindIP; } /** {@inheritDoc} */ @Override public void setBindIP(final String ip) { bindIP = ip; } /** * Get the current Value of createFake. * * @return Value of createFake (true if fake clients will be added for callbacks, else false) */ public boolean getCreateFake() { return createFake; } /** * Set the current Value of createFake. * * @param newValue New value to set createFake */ public void setCreateFake(final boolean newValue) { createFake = newValue; } /** * 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. * This returns "this" and thus can be used in the construction line. * * @param newValue New value to set addLastLine */ public void setAddLastLine(final boolean newValue) { addLastLine = 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; } /** {@inheritDoc} */ @Override public CallbackManager getCallbackManager() { return myCallbackManager; } /** * 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; } /** {@inheritDoc} */ @Override public IgnoreList getIgnoreList() { return myIgnoreList; } /** {@inheritDoc} */ @Override public void setIgnoreList(final IgnoreList ignoreList) { myIgnoreList = ignoreList; } //--------------------------------------------------------------------------- // Start Callbacks //--------------------------------------------------------------------------- /** * Callback to all objects implementing the ServerError Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IServerError * @param message The error message * @return true if a method was called, false otherwise */ protected boolean callServerError(final String message) { return myCallbackManager.getCallbackType(ServerErrorListener.class).call(message); } /** * Callback to all objects implementing the DataIn Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IDataIn * @param data Incomming Line. * @return true if a method was called, false otherwise */ protected boolean callDataIn(final String data) { return myCallbackManager.getCallbackType(DataInListener.class).call(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 * @return true if a method was called, false otherwise * @see com.dmdirc.parser.irc.callbacks.interfaces.IDataOut */ protected boolean callDataOut(final String data, final boolean fromParser) { return myCallbackManager.getCallbackType(DataOutListener.class).call(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 * @return true if a method was called, false otherwise */ protected boolean callDebugInfo(final int level, final String data, final Object... args) { return 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 * @return true if a method was called, false otherwise */ protected boolean callDebugInfo(final int level, final String data) { return myCallbackManager.getCallbackType(DebugInfoListener.class).call(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. * @return true if a method was called, false otherwise */ protected boolean callErrorInfo(final ParserError errorInfo) { return myCallbackManager.getCallbackType(ErrorInfoListener.class).call(errorInfo); } /** * Callback to all objects implementing the IConnectError Interface. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IConnectError * @param errorInfo ParserError object representing the error. * @return true if a method was called, false otherwise */ protected boolean callConnectError(final ParserError errorInfo) { return myCallbackManager.getCallbackType(ConnectErrorListener.class).call(errorInfo); } /** * Callback to all objects implementing the SocketClosed Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.ISocketClosed * @return true if a method was called, false otherwise */ protected boolean callSocketClosed() { return myCallbackManager.getCallbackType(SocketCloseListener.class).call(); } /** * Callback to all objects implementing the PingFailed Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingFailed * @return true if a method was called, false otherwise */ protected boolean callPingFailed() { return myCallbackManager.getCallbackType(PingFailureListener.class).call(); } /** * Callback to all objects implementing the PingSent Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingSent * @return true if a method was called, false otherwise */ protected boolean callPingSent() { return myCallbackManager.getCallbackType(PingSentListener.class).call(); } /** * Callback to all objects implementing the PingSuccess Callback. * * @see com.dmdirc.parser.irc.callbacks.interfaces.IPingSuccess * @return true if a method was called, false otherwise */ protected boolean callPingSuccess() { return myCallbackManager.getCallbackType(PingSuccessListener.class).call(); } /** * Callback to all objects implementing the Post005 Callback. * * @return true if any callbacks were called. * @see IPost005 */ protected synchronized boolean callPost005() { if (post005) { return false; } post005 = true; if (!h005Info.containsKey("CHANTYPES")) { parseChanPrefix(); } if (!h005Info.containsKey("PREFIX")) { parsePrefixModes(); } if (!h005Info.containsKey("USERMODES")) { parseUserModes(); } if (!h005Info.containsKey("CHANMODES")) { parseChanModes(); } return getCallbackManager().getCallbackType(Post005Listener.class).call(); } //--------------------------------------------------------------------------- // End Callbacks //--------------------------------------------------------------------------- /** Reset internal state (use before connect). */ 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; serverName = ""; networkName = ""; lastLine = ""; myself = new IRCClientInfo(this, "myself").setFake(true); synchronized (serverInformationLines) { serverInformationLines.clear(); } stopPingTimer(); currentSocketState = SocketState.CLOSED; // Char Mapping updateCharArrays((byte)4); } /** * 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; } /** * Connect to IRC. * * @throws IOException if the socket can not be connected * @throws UnknownHostException if the hostname can not be resolved * @throws NoSuchAlgorithmException if SSL is not available * @throws KeyManagementException if the trustManager is invalid */ private void connect() throws UnknownHostException, IOException, NoSuchAlgorithmException, KeyManagementException { resetState(); callDebugInfo(DEBUG_SOCKET, "Connecting to " + server.getHost() + ":" + server.getPort()); if (server.getPort() > 65535 || server.getPort() <= 0) { throw new IOException("Server port ("+server.getPort()+") is invalid."); } if (server.getUseSocks()) { callDebugInfo(DEBUG_SOCKET, "Using Proxy"); if (bindIP != null && !bindIP.isEmpty()) { callDebugInfo(DEBUG_SOCKET, "IP Binding is not possible when using a proxy."); } if (server.getProxyPort() > 65535 || server.getProxyPort() <= 0) { throw new IOException("Proxy port ("+server.getProxyPort()+") is invalid."); } final Proxy.Type proxyType = Proxy.Type.SOCKS; socket = new Socket(new Proxy(proxyType, new InetSocketAddress(server.getProxyHost(), server.getProxyPort()))); currentSocketState = SocketState.OPENING; if (server.getProxyUser() != null && !server.getProxyUser().isEmpty()) { IRCAuthenticator.getIRCAuthenticator().addAuthentication(server); } socket.connect(new InetSocketAddress(server.getHost(), server.getPort())); } else { callDebugInfo(DEBUG_SOCKET, "Not using Proxy"); if (!server.getSSL()) { socket = new Socket(); if (bindIP != null && !bindIP.isEmpty()) { callDebugInfo(DEBUG_SOCKET, "Binding to IP: "+bindIP); try { socket.bind(new InetSocketAddress(InetAddress.getByName(bindIP), 0)); } catch (IOException e) { callDebugInfo(DEBUG_SOCKET, "Binding failed: "+e.getMessage()); } } currentSocketState = SocketState.OPENING; socket.connect(new InetSocketAddress(server.getHost(), server.getPort())); } } if (server.getSSL()) { 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(); if (server.getUseSocks()) { socket = socketFactory.createSocket(socket, server.getHost(), server.getPort(), false); } else { if (bindIP == null || bindIP.isEmpty()) { socket = socketFactory.createSocket(server.getHost(), server.getPort()); } else { callDebugInfo(DEBUG_SOCKET, "Binding to IP: "+bindIP); try { socket = socketFactory.createSocket(server.getHost(), server.getPort(), InetAddress.getByName(bindIP), 0); } catch (UnknownHostException e) { callDebugInfo(DEBUG_SOCKET, "Bind failed: "+e.getMessage()); socket = socketFactory.createSocket(server.getHost(), server.getPort()); } } } 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 BufferedReader(new InputStreamReader(socket.getInputStream())); callDebugInfo(DEBUG_SOCKET, "\t-> Socket Opened"); } /** * Send server connection strings (NICK/USER/PASS). */ protected void sendConnectionStrings() { if (!server.getPassword().isEmpty()) { sendString("PASS " + server.getPassword()); } sendString("NICK " + me.getNickname()); thinkNickname = me.getNickname(); String localhost; try { localhost = InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException uhe) { localhost = "*"; } sendString("USER " + me.getUsername() + " "+localhost+" "+server.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 incomming lines */ @Override public void run() { callDebugInfo(DEBUG_INFO, "Begin Thread Execution"); if (hasBegan) { return; } else { hasBegan = true; } try { connect(); } 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 { socket.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(String channel) { synchronized (channelList) { channel = getStringConverter().toLowerCase(channel); if (channelList.containsKey(channel)) { return channelList.get(channel); } else { return null; } } } /** {@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. * * @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) { // 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 LinkedList foundModes = new LinkedList(); final IRCChannelInfo channel = getChannel(newLine[1]); if (channel != null) { 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 getServerName() { return serverName; } /** {@inheritDoc} */ @Override public String getLastLine() { return lastLine; } /** * Get the list of lines the server gave from 001 - 005. * * @return List of lines sent by the server between 001 and 005 inclusive. */ public List getServerInformationLines() { synchronized (serverInformationLines) { return new LinkedList(serverInformationLines); } } /** * Process a line and call relevent methods for handling. * * @param line IRC Line to process */ protected void processLine(final String line) { callDataIn(line); final String[] token = tokeniseLine(line); int nParam; setPingNeeded(false); // pingCountDown = pingCountDownLength; 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 (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("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); } } } // After 001 we potentially care about everything! try { myProcessingManager.process(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 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(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; } // Otherwise, send to Notice Auth try { myProcessingManager.process("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((byte)4); } return stringConverter; } /** * Update the character arrays. * * @param limit Number of post-alphabetical characters to convert * 0 = ascii encoding * 3 = strict-rfc1459 encoding * 4 = rfc1459 encoding */ protected void updateCharArrays(final byte limit) { stringConverter = new IRCStringConverter(limit); } /** * Get the known boolean chanmodes in 005 order. * Modes are returned in the order that the ircd specifies the modes in 005 * with any newly-found modes (mode being set that wasn't specified in 005) * being added at the end. * * @return All the currently known boolean modes */ public String getBoolChanModes005() { // This code isn't the nicest, as Hashtable's don't lend themselves to being // ordered. // Order isn't really important, and this code only takes 3 lines of we // don't care about it but ordered guarentees that on a specific ircd this // method will ALWAYs return the same value. final char[] modes = new char[chanModesBool.size()]; long nTemp; double pos; for (char cTemp : chanModesBool.keySet()) { nTemp = chanModesBool.get(cTemp); // nTemp should never be less than 0 if (nTemp > 0) { pos = Math.log(nTemp) / Math.log(2); modes[(int)pos] = cTemp; } /* // Is there an easier way to find out the power of 2 value for a number? // ie 1024 = 10, 512 = 9 ? for (int i = 0; i < modes.length; i++) { if (Math.pow(2, i) == (double) nTemp) { modes[i] = cTemp; break; } }*/ } return new String(modes); } /** * 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 (getIRCD(true).equalsIgnoreCase("dancer")) { sDefaultModes.insert(0, "dqeI"); } else if (getIRCD(true).equalsIgnoreCase("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 = 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)); } /** * 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 = 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.containsKey(cMode)) { chanPrefix.put(cMode, true); } } } /** * 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 = nextKeyPrefix * 2; } } h005Info.put("PREFIXSTRING", bits[0]); } /** * Check if server is ready. * * @return true if 001 has been recieved, false otherwise. */ public boolean isReady() { return got001; } /** {@inheritDoc} */ @Override public void joinChannel(final String channel) { joinChannel(channel, "", true); } /** * Join a Channel. * * @param sChannelName Name of channel to join * @param autoPrefix Automatically prepend the first channel prefix defined * in 005 if sChannelName is an invalid channel. * **This only applies to the first channel if given a list** */ public void joinChannel(final String sChannelName, final boolean autoPrefix) { joinChannel(sChannelName, "", autoPrefix); } /** {@inheritDoc} */ @Override public void joinChannel(final String channel, final String key) { joinChannel(channel, key, true); } /** * Join a Channel with a key. * * @param channel Name of channel to join * @param key Key to use to try and join the channel * @param autoPrefix Automatically prepend the first channel prefix defined * in 005 if sChannelName is an invalid channel. * **This only applies to the first channel if given a list** */ public void joinChannel(final String channel, final String key, final boolean autoPrefix) { final String channelName; if (isValidChannelName(channel)) { channelName = channel; } else { if (autoPrefix) { if (h005Info.containsKey("CHANTYPES")) { final String chantypes = h005Info.get("CHANTYPES"); if (chantypes.isEmpty()) { channelName = "#" + channel; } else { channelName = chantypes.charAt(0) + channel; } } else { return; } } else { return; } } if (key.isEmpty()) { sendString("JOIN " + channelName); } else { sendString("JOIN " + channelName + " " + key); } } /** * 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; } if (reason.isEmpty()) { sendString("PART " + channel); } else { 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 = length + type.length(); } if (target != null) { length = 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 && getIRCD(true).equalsIgnoreCase("weircd")) { // -_- result = 50; } else if (result == -2) { result = -1; callDebugInfo(DEBUG_INFO, "Failed"); callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "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()/* || sMessage.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()/* || sMessage.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); } /** * Quit IRC. * This method will wait for the server to close the socket. * * @param reason Reason for quitting. */ public void quit(final String reason) { if (reason.isEmpty()) { sendString("QUIT"); } else { sendString("QUIT :" + reason); } } /** {@inheritDoc} */ @Override public void disconnect(final String message) { if (currentSocketState == SocketState.OPENING || currentSocketState == SocketState.OPEN) { currentSocketState = SocketState.CLOSING; if (got001) { quit(message); } } try { if (socket != null) { socket.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.containsKey(name.charAt(0)) || name.equals("0"); } /** {@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 name of the ircd. * * @param getType if this is false the string from 004 is returned. Else a guess of the type (ircu, hybrid, ircnet) * @return IRCD Version or Type */ public String getIRCD(final boolean getType) { if (h005Info.containsKey("004IRCD")) { final String version = h005Info.get("004IRCD"); if (getType) { // This ilst is vaugly based on http://searchirc.com/ircd-versions, // but keeping groups of ircd's together (ie hybrid-based, ircu-based) if (version.matches("(?i).*unreal[^4-9].*")) { return "unreal"; } else if (version.matches("(?i).*unreal[4-9].*")) { return "unreal4"; } else if (version.matches("(?i).*bahamut.*")) { return "bahamut"; } else if (version.matches("(?i).*nefarious.*")) { return "nefarious"; } else if (version.matches("(?i).*asuka.*")) { return "asuka"; } else if (version.matches("(?i).*snircd.*")) { return "snircd"; } else if (version.matches("(?i).*beware.*")) { return "bircd"; } else if (version.matches("(?i).*u2\\.[0-9]+\\.H\\..*")) { return "irchispano"; } else if (version.matches("(?i).*u2\\.[0-9]+\\..*")) { return "ircu"; } else if (version.matches("(?i).*ircu.*")) { return "ircu"; } else if (version.matches("(?i).*plexus.*")) { return "plexus"; } else if (version.matches("(?i).*hybrid.*oftc.*")) { return "oftc-hybrid"; } else if (version.matches("(?i).*ircd.hybrid.*")) { return "hybrid7"; } else if (version.matches("(?i).*hybrid.*")) { return "hybrid"; } else if (version.matches("(?i).*charybdis.*")) { return "charybdis"; } else if (version.matches("(?i).*inspircd.*")) { return "inspircd"; } else if (version.matches("(?i).*ultimateircd.*")) { return "ultimateircd"; } else if (version.matches("(?i).*critenircd.*")) { return "critenircd"; } else if (version.matches("(?i).*fqircd.*")) { return "fqircd"; } else if (version.matches("(?i).*conferenceroom.*")) { return "conferenceroom"; } else if (version.matches("(?i).*hyperion.*")) { return "hyperion"; } else if (version.matches("(?i).*dancer.*")) { return "dancer"; } else if (version.matches("(?i).*austhex.*")) { return "austhex"; } else if (version.matches("(?i).*austirc.*")) { return "austirc"; } else if (version.matches("(?i).*ratbox.*")) { return "ratbox"; } else if (version.matches("(?i).*euircd.*")) { return "euircd"; } else if (version.matches("(?i).*weircd.*")) { return "weircd"; } else if (version.matches("(?i).*swiftirc.*")) { return "swiftirc"; } else { // Stupid networks/ircds go here... if (networkName.equalsIgnoreCase("ircnet")) { return "ircnet"; } else if (networkName.equalsIgnoreCase("starchat")) { return "starchat"; } else if (networkName.equalsIgnoreCase("bitlbee")) { return "bitlbee"; } else if (h005Info.containsKey("003IRCD") && h005Info.get("003IRCD").matches("(?i).*bitlbee.*")) { return "bitlbee"; } // Older bitlbee else if (h005Info.containsKey("002IRCD") && h005Info.get("002IRCD").matches("(?i).*pastiche.*")) { return "ircd-pastiche"; } else { return "generic"; } } } else { return version; } } else { if (getType) { return "generic"; } else { return ""; } } } /** {@inheritDoc} */ @Override public String getServerSoftware() { return getIRCD(false); } /** {@inheritDoc} */ @Override public String getServerSoftwareType() { return getIRCD(true); } /** * 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 long getPingTimerInterval() { return pingTimerLength; } /** {@inheritDoc} */ @Override public void setPingTimerInterval(final long newValue) { pingTimerLength = newValue; startPingTimer(); } /** {@inheritDoc} */ @Override public int getPingTimerFraction() { return pingCountDownLength; } /** {@inheritDoc} */ @Override public void setPingTimerFraction(final int newValue) { pingCountDownLength = newValue; } /** * Start the pingTimer. */ protected void startPingTimer() { pingTimerSem.acquireUninterruptibly(); setPingNeeded(false); if (pingTimer != null) { pingTimer.cancel(); } pingTimer = new Timer("IRCParser pingTimer"); pingTimer.schedule(new PingTimer(this, pingTimer), 0, pingTimerLength); pingCountDown = 1; 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 = pingCountDownLength; lastPingValue = String.valueOf(System.currentTimeMillis()); if (sendString("PING " + lastPingValue, QueuePriority.HIGH)) { callPingSent(); } } } } /** {@inheritDoc} */ @Override public long getServerLatency() { return serverLag; } /** * 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(); } } /** * 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; } }