/* * Copyright (c) 2006-2017 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.BaseSocketAwareParser; 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.events.ConnectErrorEvent; import com.dmdirc.parser.events.DebugInfoEvent; import com.dmdirc.parser.events.ErrorInfoEvent; import com.dmdirc.parser.events.PingFailureEvent; import com.dmdirc.parser.events.PingSentEvent; import com.dmdirc.parser.events.PingSuccessEvent; import com.dmdirc.parser.events.ServerErrorEvent; import com.dmdirc.parser.events.ServerReadyEvent; import com.dmdirc.parser.events.SocketCloseEvent; import com.dmdirc.parser.interfaces.ChannelInfo; import com.dmdirc.parser.interfaces.Encoder; import com.dmdirc.parser.interfaces.EncodingParser; import com.dmdirc.parser.interfaces.SecureParser; import com.dmdirc.parser.irc.IRCReader.ReadLine; import com.dmdirc.parser.irc.events.IRCDataInEvent; import com.dmdirc.parser.irc.events.IRCDataOutEvent; import com.dmdirc.parser.irc.outputqueue.OutputQueue; import com.dmdirc.parser.irc.outputqueue.PriorityOutputQueue; import java.io.IOException; import java.net.InetAddress; 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.SecureRandom; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; 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 dagger.ObjectGraph; import static com.google.common.base.Preconditions.checkNotNull; /** * IRC Parser. */ @ChildImplementations({ IRCChannelClientInfo.class, IRCChannelInfo.class, IRCClientInfo.class }) public class IRCParser extends BaseSocketAwareParser 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. */ public static final boolean ALWAYS_UPDATECLIENT = true; /** Byte used to show that a non-boolean mode is a list (b). */ public 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). */ public static final byte MODE_UNSET = 4; /** server-time time format */ private static final DateTimeFormatter serverTimeFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); /** * Default channel prefixes if none are specified by the IRCd. * *

These are the RFC 2811 specified prefixes: '#', '&', '!' and '+'. */ private static final String DEFAULT_CHAN_PREFIX = "#&!+"; /** * 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; /** 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 #setPingTimerInterval */ private int pingCountDown; /** Network name. This is "" if no network name is provided */ public String networkName; /** This is what we think the nickname should be. */ public String thinkNickname; /** Have we received the 001. */ public boolean got001; /** Have we fired post005? */ boolean post005; /** Has the thread started execution yet, (Prevents run() being called multiple times). */ boolean hasBegan; /** Manager used to handle prefix modes. */ private final PrefixModeManager prefixModes = new PrefixModeManager(); /** Manager used to handle user modes (owxis etc). */ private final ModeManager userModes = new ModeManager(); /** * Manager used to handle channel boolean modes. *

* Channel modes discovered but not listed in 005 are stored as boolean modes automatically (and a ERROR_WARNING Error is called) */ private final ModeManager chanModesBool = new ModeManager(); /** * 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
*/ public final Map chanModesOther = new HashMap<>(); /** The last line of input received from the server */ private ReadLine lastLine; /** Should the lastline (where given) be appended to the "data" part of any onErrorInfo call? */ private boolean addLastLine; /** Channel Prefixes (ie # + etc). */ private String chanPrefix = DEFAULT_CHAN_PREFIX; /** 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; /** Hashtable storing all information gathered from 005. */ public final Map h005Info = new HashMap<>(); /** difference in ms between our time and the servers time (used for timestampedIRC). */ private long tsdiff; /** Reference to the Processing Manager. */ private final ProcessingManager myProcessingManager; /** Should we automatically disconnect on fatal errors?. */ private boolean disconnectOnFatal = true; /** Current Socket State. */ protected SocketState currentSocketState = SocketState.NULL; /** * The underlying socket connected to the IRC server. For SSL connections this socket will be * wrapped, and should therefore not be used to send or receive data. */ private Socket rawSocket; /** The socket connected to the IRC server, used for sending or receiving data. */ private Socket socket; /** 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 TrustingTrustManager()}; /** 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<>(); /** Handler for whois responses. */ private final WhoisResponseHandler whoisHandler; /** Used to synchronize calls to resetState. */ private final Object resetStateSync = new Object(); /** * 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); setCallbackManager(new IRCParserCallbackManager(this::handleCallbackError)); // TODO: There should be a factory or builder for parsers that can construct the graph final ObjectGraph graph = ObjectGraph.create(new IRCParserModule(this, prefixModes, userModes, chanModesBool)); myProcessingManager = graph.get(ProcessingManager.class); myself = new IRCClientInfo(this, userModes, "myself").setFake(true); out = new PriorityOutputQueue(); if (myDetails != null) { this.me = myDetails; } this.whoisHandler = new WhoisResponseHandler(this, getCallbackManager()); setIgnoreList(new IgnoreList()); setPingTimerInterval(10000); setPingTimerFraction(6); resetState(); } /** * Get the current OutputQueue * * @return the current OutputQueue */ public OutputQueue getOutputQueue() { return out; } /** * Sets the output queue that this parser will use. * *

Any existing items in the queue will be removed. * * @param queue The queue to be added. */ public void setOutputQueue(final OutputQueue queue) throws IOException { checkNotNull(queue); out.clearQueue(); if (socket != null) { queue.setOutputStream(socket.getOutputStream()); } out = queue; } @Override public boolean compareURI(final URI uri) { // Get the old URI. final URI oldURI = getURI(); // 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 uri.getScheme().equalsIgnoreCase(oldURI.getScheme()) && uri.getHost().equalsIgnoreCase(oldURI.getHost()) && (uri.getUserInfo() == null || uri.getUserInfo().isEmpty() || uri.getUserInfo().equalsIgnoreCase(oldURI.getUserInfo() == null ? "" : oldURI.getUserInfo())) && uri.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 ? IrcConstants.DEFAULT_SSL_PORT : IrcConstants.DEFAULT_PORT; // 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; } } @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 Collection 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 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 Arrays.copyOf(trustAllCerts, trustAllCerts.length); } /** * Get a reference to the current TrustManager for SSL Sockets. * * @return a reference to myTrustManager; */ public TrustManager[] getTrustManager() { return Arrays.copyOf(myTrustManager, myTrustManager.length); } @Override public void setTrustManagers(final TrustManager... managers) { myTrustManager = managers == null ? null : Arrays.copyOf(managers, managers.length); } @Override public void setKeyManagers(final KeyManager... managers) { myKeyManagers = managers == null ? null : Arrays.copyOf(managers, managers.length); } //--------------------------------------------------------------------------- // Start Callbacks //--------------------------------------------------------------------------- /** * Callback to all objects implementing the ServerError Callback. * * @param message The error message */ protected void callServerError(final String message) { getCallbackManager().publish(new ServerErrorEvent(this, LocalDateTime.now(), message)); } /** * Callback to all objects implementing the DataIn Callback. * * @param line Incoming Line. */ protected void callDataIn(final ReadLine line) { getCallbackManager().publish(new IRCDataInEvent(this, LocalDateTime.now(), line)); } /** * 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 */ protected void callDataOut(final String data, final boolean fromParser) { getCallbackManager().publish(new IRCDataOutEvent(this, LocalDateTime.now(), data)); } /** * Callback to all objects implementing the DebugInfo Callback. * * @param level Debugging Level (DEBUG_INFO, DEBUG_SOCKET etc) * @param data Debugging Information as a format string * @param args Formatting String Options */ public void callDebugInfo(final int level, final String data, final Object... args) { callDebugInfo(level, args == null || args.length == 0 ? data : String.format(data, args)); } /** * Callback to all objects implementing the DebugInfo Callback. * * @param level Debugging Level (DEBUG_INFO, DEBUG_SOCKET etc) * @param data Debugging Information */ protected void callDebugInfo(final int level, final String data) { getCallbackManager().publish(new DebugInfoEvent(this, LocalDateTime.now(), level, data)); } /** * Callback to all objects implementing the IErrorInfo Interface. * * @param errorInfo ParserError object representing the error. */ public void callErrorInfo(final ParserError errorInfo) { getCallbackManager().publish(new ErrorInfoEvent(this, LocalDateTime.now(), errorInfo)); } /** * Callback to all objects implementing the IConnectError Interface. * * @param errorInfo ParserError object representing the error. */ protected void callConnectError(final ParserError errorInfo) { getCallbackManager().publish(new ConnectErrorEvent(this, LocalDateTime.now(), errorInfo)); } /** * Callback to all objects implementing the SocketClosed Callback. */ protected void callSocketClosed() { // Don't allow state resetting whilst there may be handlers requiring // state. synchronized (resetStateSync) { getCallbackManager().publish(new SocketCloseEvent(this, LocalDateTime.now())); } } /** * Callback to all objects implementing the PingFailed Callback. */ protected void callPingFailed() { getCallbackManager().publish(new PingFailureEvent(this, LocalDateTime.now())); } /** * Callback to all objects implementing the PingSent Callback. */ protected void callPingSent() { getCallbackManager().publish(new PingSentEvent(this, LocalDateTime.now())); } /** * Callback to all objects implementing the PingSuccess Callback. */ protected void callPingSuccess() { getCallbackManager().publish(new PingSuccessEvent(this, LocalDateTime.now())); } /** * Callback to all objects implementing the Post005 Callback. */ protected synchronized void callPost005() { if (post005) { return; } post005 = true; if (!h005Info.containsKey(IrcConstants.ISUPPORT_CHANNEL_USER_PREFIXES)) { parsePrefixModes(); } if (!h005Info.containsKey(IrcConstants.ISUPPORT_USER_MODES)) { parseUserModes(); } if (!h005Info.containsKey(IrcConstants.ISUPPORT_CHANNEL_MODES)) { parseChanModes(); } whoisHandler.start(); getCallbackManager().publish(new ServerReadyEvent(this, LocalDateTime.now())); } //--------------------------------------------------------------------------- // End Callbacks //--------------------------------------------------------------------------- /** Reset internal state (use before doConnect). */ private void resetState() { synchronized (resetStateSync) { // Reset General State info got001 = false; post005 = false; // Clear the hash tables channelList.clear(); clientList.clear(); h005Info.clear(); prefixModes.clear(); chanModesOther.clear(); chanModesBool.clear(); userModes.clear(); chanPrefix = DEFAULT_CHAN_PREFIX; // Clear output queue. out.clearQueue(); setServerName(""); networkName = ""; lastLine = null; myself = new IRCClientInfo(this, userModes, "myself").setFake(true); synchronized (serverInformationLines) { serverInformationLines.clear(); } stopPingTimer(); currentSocketState = SocketState.CLOSED; setEncoding(IRCEncoding.RFC1459); whoisHandler.stop(); } } /** * 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 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 %s:%s", getURI().getHost(), getURI().getPort()); currentSocketState = SocketState.OPENING; final URI connectUri = getConnectURI(getURI()); rawSocket = getSocketFactory().createSocket(connectUri.getHost(), connectUri.getPort()); 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 SecureRandom()); final SSLSocketFactory socketFactory = sc.getSocketFactory(); socket = socketFactory.createSocket(rawSocket, 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; } else { socket = rawSocket; } 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 (%s), Aborted", e.getMessage()); 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 (IOException e) { handleConnectException(e, true); return; } catch (NoSuchAlgorithmException | 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 (%s), Aborted", e.getMessage()); if (currentSocketState != SocketState.CLOSED) { currentSocketState = SocketState.CLOSED; callSocketClosed(); } resetState(); break; } } callDebugInfo(DEBUG_INFO, "End Thread Execution"); } @Override public void shutdown() { try { // See note at disconnect() method for why we close rawSocket. if (rawSocket != null) { rawSocket.close(); socket = null; rawSocket = null; } } catch (IOException e) { callDebugInfo(DEBUG_SOCKET, "Could not close socket"); } super.shutdown(); } /** * Get the trailing parameter for a line. * The parameter is everything after the first occurrence of " :" to 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) { final String[] 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 || line.isEmpty()) { return new String[]{""}; } int lastarg = line.indexOf(" :"); String[] tokens; // Check for IRC Tags. if (line.charAt(0) == '@') { // We have tags. boolean hasIRCv3Tags = true; tokens = line.split(" ", 2); final int tsEnd = tokens[0].indexOf('@', 1); if (tsEnd > -1) { try { final long ts = Long.parseLong(tokens[0].substring(1, tsEnd)); // We might have both IRCv3 tags and TSIRC, with TSIRC first, eg: // @123@@tag=value :test ing hasIRCv3Tags = tokens[0].length() > tsEnd && tokens[0].charAt(tsEnd) == '@'; } catch (final NumberFormatException nfe) { /* Not a timestamp. */ } } if (hasIRCv3Tags) { // IRCv3 Tags, last arg is actually the second " :" lastarg = line.indexOf(" :", lastarg+1); } } 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; } @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, userModes, details).setFake(true); } } public boolean isKnownClient(final String host) { final String sWho = getStringConverter().toLowerCase(IRCClientInfo.parseHost(host)); return clientList.containsKey(sWho); } @Override public IRCChannelInfo getChannel(final String channel) { synchronized (channelList) { return channelList.get(getStringConverter().toLowerCase(channel)); } } @Override public void sendInvite(final String channel, final String user) { sendRawMessage("INVITE " + user + ' ' + channel); } @Override public void sendWhois(final String nickname) { sendRawMessage("WHOIS " + nickname); } @Override public void sendRawMessage(final String message) { sendString(message, QueuePriority.NORMAL, false); } @Override public void sendRawMessage(final String message, final QueuePriority priority) { sendString(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. */ public boolean sendString(final String line) { return sendString(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. */ public boolean sendString(final String line, final QueuePriority priority) { return sendString(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 sendString(final String line, final QueuePriority priority, final boolean fromParser) { if (getSocketState() != SocketState.OPEN) { return false; } callDataOut(line, fromParser); out.sendLine(line, priority); parseOutgoingLine(line); return true; } /** * Parses a line that has been sent to the server in order to track state. * * @param line The line to be parsed. */ private void parseOutgoingLine(final String line) { final String[] newLine = tokeniseLine(line); if ("away".equalsIgnoreCase(newLine[0]) && newLine.length > 1) { myself.setAwayReason(newLine[newLine.length - 1]); } else if ("mode".equalsIgnoreCase(newLine[0]) && 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 Queue 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 %s for mode %s", channel, 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"); } } } } } } @Override public String getNetworkName() { return networkName; } @Override public String getLastLine() { return lastLine == null ? "" : lastLine.getLine(); } @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); final String[] token = line.getTokens(); LocalDateTime lineTS = LocalDateTime.now(); if (line.getTags().containsKey("tsirc date")) { try { final long ts = Long.parseLong(line.getTags().get("tsirc date")) - tsdiff; lineTS = LocalDateTime.ofInstant(Instant.ofEpochSecond(ts / 1000L, (int) (ts % 1000L)), ZoneId.systemDefault()); } catch (final NumberFormatException nfe) { /* Do nothing. */ } } else if (line.getTags().containsKey("time")) { try { lineTS = OffsetDateTime.parse(line.getTags().get("time"), serverTimeFormat) .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); } catch (final DateTimeParseException pe) { /* Do nothing. */ } } setPingNeeded(false); if (token.length < 2) { return; } try { final String sParam = token[1]; if ("PING".equalsIgnoreCase(token[0]) || "PING".equalsIgnoreCase(token[1])) { sendString("PONG :" + sParam, QueuePriority.HIGH); } else if ("PONG".equalsIgnoreCase(token[0]) || "PONG".equalsIgnoreCase(token[1])) { if (!lastPingValue.isEmpty() && lastPingValue.equals(token[token.length - 1])) { lastPingValue = ""; serverLag = System.currentTimeMillis() - pingTime; callPingSuccess(); } } else if ("ERROR".equalsIgnoreCase(token[0])) { final StringBuilder errorMessage = new StringBuilder(); for (int i = 1; i < token.length; ++i) { errorMessage.append(token[i]); } callServerError(errorMessage.toString()); } else if ("TSIRC".equalsIgnoreCase(token[1]) && token.length > 3) { if ("1".equals(token[2])) { try { final long ts = Long.parseLong(token[3]); tsdiff = ts - System.currentTimeMillis(); } catch (final NumberFormatException nfe) { /* Do nothing. */ } } } else { int nParam; 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 ("NOTICE".equalsIgnoreCase(token[0]) || token.length > 2 && "NOTICE".equalsIgnoreCase(token[2])) { 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 IrcConstants.NUMERIC_ERROR_PASSWORD_MISMATCH: case IrcConstants.NUMERIC_ERROR_NICKNAME_IN_USE: try { myProcessingManager.process(lineTS, 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 ("NICK".equalsIgnoreCase(token[1])) { break; } // CAP also happens here, so try that. if ("CAP".equalsIgnoreCase(token[1])) { myProcessingManager.process(lineTS, sParam, token); break; } // 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; @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 */ public void setEncoding(final IRCEncoding encoding) { stringConverter = new IRCStringConverter(encoding); } /** * Check the state of the requested capability. * * @param capability The capability to check the state of. * @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 modeStr; if (h005Info.containsKey(IrcConstants.ISUPPORT_USER_CHANNEL_MODES)) { if (getServerType() == ServerType.DANCER) { sDefaultModes.insert(0, "dqeI"); } else if (getServerType() == ServerType.AUSTIRC) { sDefaultModes.insert(0, 'e'); } modeStr = h005Info.get(IrcConstants.ISUPPORT_USER_CHANNEL_MODES); for (int i = 0; i < modeStr.length(); ++i) { final char mode = modeStr.charAt(i); if (!prefixModes.isPrefixMode(mode) && sDefaultModes.indexOf(Character.toString(mode)) < 0) { sDefaultModes.append(mode); } } } else { sDefaultModes.append("imnpstrc"); } if (h005Info.containsKey(IrcConstants.ISUPPORT_CHANNEL_MODES)) { modeStr = h005Info.get(IrcConstants.ISUPPORT_CHANNEL_MODES); } else { modeStr = sDefaultModes.toString(); h005Info.put(IrcConstants.ISUPPORT_CHANNEL_MODES, modeStr); } String[] 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(IrcConstants.ISUPPORT_CHANNEL_MODES, modeStr); bits = modeStr.split(",", 5); } // resetState chanModesOther.clear(); // 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 chanModesBool.set(bits[3]); callDebugInfo(DEBUG_INFO, "Found boolean modes: %s", bits[3]); } @Override public String getChannelUserModes() { return prefixModes.getPrefixes(); } @Override public String getBooleanChannelModes() { return chanModesBool.getModes(); } @Override public String getListChannelModes() { return getOtherModeString(MODE_LIST); } @Override public String getParameterChannelModes() { return getOtherModeString(MODE_SET); } @Override public String getDoubleParameterChannelModes() { return getOtherModeString((byte) (MODE_SET + MODE_UNSET)); } @Override public String getChannelPrefixes() { return chanPrefix; } /** * 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()]; int i = 0; for (char cTemp : chanModesOther.keySet()) { final Byte nTemp = chanModesOther.get(cTemp); if (nTemp == value) { modes[i] = cTemp; i++; } } // Alphabetically sort the array Arrays.sort(modes); return new String(modes).trim(); } @Override public String getUserModes() { if (h005Info.containsKey(IrcConstants.ISUPPORT_USER_MODES)) { return h005Info.get(IrcConstants.ISUPPORT_USER_MODES); } else { return ""; } } /** * Process USERMODES from 004. */ public void parseUserModes() { final String modeStr; if (h005Info.containsKey(IrcConstants.ISUPPORT_USER_MODES)) { modeStr = h005Info.get(IrcConstants.ISUPPORT_USER_MODES); } else { final String sDefaultModes = "nwdoi"; modeStr = sDefaultModes; h005Info.put(IrcConstants.ISUPPORT_USER_MODES, sDefaultModes); } userModes.set(modeStr); } /** * Resets the channel prefix property to the default, RFC specified value. */ public void resetChanPrefix() { chanPrefix = DEFAULT_CHAN_PREFIX; } /** * Sets the set of possible channel prefixes to those in the given value. * * @param value The new set of channel prefixes. */ public void setChanPrefix(final String value) { chanPrefix = value; } /** * Process PREFIX from 005. */ public void parsePrefixModes() { final String sDefaultModes = "(ohv)@%+"; String modeStr; if (h005Info.containsKey(IrcConstants.ISUPPORT_CHANNEL_USER_PREFIXES)) { modeStr = h005Info.get(IrcConstants.ISUPPORT_CHANNEL_USER_PREFIXES); } else { modeStr = sDefaultModes; } if ("(".equals(modeStr.substring(0, 1))) { modeStr = modeStr.substring(1); } else { modeStr = sDefaultModes.substring(1); h005Info.put(IrcConstants.ISUPPORT_CHANNEL_USER_PREFIXES, sDefaultModes); } int closingIndex = modeStr.indexOf(')'); if (closingIndex * 2 + 1 != modeStr.length()) { callErrorInfo(new ParserError(ParserError.ERROR_ERROR, "PREFIX String not valid. Using default string of \"" + modeStr + '"', getLastLine())); h005Info.put(IrcConstants.ISUPPORT_CHANNEL_USER_PREFIXES, sDefaultModes); modeStr = sDefaultModes.substring(1); closingIndex = modeStr.indexOf(')'); } // The modes passed from the server are in descending order of importance, we want to // store them in ascending, so reverse them: final String reversedModes = new StringBuilder(modeStr).reverse().toString(); prefixModes.setModes(reversedModes.substring(closingIndex + 1), reversedModes.substring(0, closingIndex)); } @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.computeIfAbsent(channel.getPassword(), k -> new StringBuffer()); // 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 (chanPrefix.isEmpty()) { // TODO: This is wrong - empty chan prefix means the // IRCd supports no channels. list.append('#'); } else { list.append(chanPrefix.charAt(0)); } } 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; } @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; } } @Override public int getMaxListModes(final char mode) { // MAXLIST=bdeI:50 // MAXLIST=b:60,e:60,I:60 // MAXBANS=30 callDebugInfo(DEBUG_INFO, "Looking for maxlistmodes for: %s", mode); // Try in MAXLIST int result = -2; if (h005Info.get(IrcConstants.ISUPPORT_MAXIMUM_LIST_MODES) != null) { if (h005Info.get(IrcConstants.ISUPPORT_MAXIMUM_BANS) == null) { result = 0; } final String maxlist = h005Info.get(IrcConstants.ISUPPORT_MAXIMUM_LIST_MODES); callDebugInfo(DEBUG_INFO, "Found maxlist (%s)", maxlist); final String[] bits = maxlist.split(","); for (String bit : bits) { final String[] parts = bit.split(":", 2); callDebugInfo(DEBUG_INFO, "Bit: %s | parts.length = %s (%s -> %s)", bit, parts.length, parts[0], parts[0].indexOf(mode)); if (parts.length == 2 && parts[0].indexOf(mode) > -1) { callDebugInfo(DEBUG_INFO, "parts[0] = '%s' | parts[1] = '%s'", parts[0], 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(IrcConstants.ISUPPORT_MAXIMUM_BANS) != null) { callDebugInfo(DEBUG_INFO, "Trying max bans"); try { result = Integer.parseInt(h005Info.get(IrcConstants.ISUPPORT_MAXIMUM_BANS)); } 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: %s", result); return result; } @Override public void sendMessage(final String target, final String message) { if (target == null || message == null) { return; } if (target.isEmpty()) { return; } sendString("PRIVMSG " + target, message); } @Override public void sendNotice(final String target, final String message) { if (target == null || message == null) { return; } if (target.isEmpty()) { return; } sendString("NOTICE " + target, message); } @Override public void sendAction(final String target, final String message) { sendCTCP(target, "ACTION", message); } @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); } @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); } @Override public void requestGroupList(final String searchTerms) { sendString("LIST", searchTerms); } @Override public void quit(final String reason) { // Don't attempt to send anything further. getOutputQueue().clearQueue(); final String output = (reason == null || reason.isEmpty()) ? "QUIT" : "QUIT :" + reason; sendString(output, QueuePriority.IMMEDIATE, true); // Don't bother queueing anything else. getOutputQueue().setDiscarding(true); } @Override public void disconnect(final String message) { super.disconnect(message); if (currentSocketState == SocketState.OPENING || currentSocketState == SocketState.OPEN) { if (got001) { quit(message); } currentSocketState = SocketState.CLOSING; } 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; } // Otherwise return true if: // Channel equals "0" // first character of the channel name is a valid channel prefix. return chanPrefix.indexOf(name.charAt(0)) >= 0 || "0".equals(name); } @Override public boolean isUserSettable(final char mode) { final String validmodes = h005Info.containsKey(IrcConstants.ISUPPORT_USER_CHANNEL_MODES) ? h005Info.get(IrcConstants.ISUPPORT_USER_CHANNEL_MODES) : "bklimnpstrc"; return validmodes.indexOf(mode) > -1; } /** * Get the 005 info. * * @return 005Info hashtable. */ public Map get005() { return Collections.unmodifiableMap(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")); } @Override public String getServerSoftware() { final String version = h005Info.get("004IRCD"); return version == null ? "" : version; } @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. */ public void setCheckServerPing(final boolean newValue) { checkServerPing = newValue; if (checkServerPing) { startPingTimer(); } else { stopPingTimer(); } } @Override public void setEncoder(final Encoder encoder) { this.encoder = encoder; } @Override public void setPingTimerInterval(final long newValue) { super.setPingTimerInterval(newValue); startPingTimer(); } /** * Start the pingTimer. */ public 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()) { callPingFailed(); } else { --pingCountDown; if (pingCountDown < 1) { pingTime = System.currentTimeMillis(); setPingNeeded(true); pingCountDown = getPingTimerFraction(); lastPingValue = String.valueOf(System.currentTimeMillis()); if (sendString("PING " + lastPingValue, QueuePriority.HIGH)) { callPingSent(); } } } } @Override public long getServerLatency() { return serverLag; } /** * Updates the name of the server that this parser is connected to. * * @param serverName The discovered server name */ public 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; } } @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. */ boolean getPingNeeded() { return pingNeeded.get(); } @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 */ public 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 ChannelInfo 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(); } } @Override public Collection getChannels() { synchronized (channelList) { return channelList.values(); } } /** * Clear the channel list. */ public void clearChannels() { synchronized (channelList) { channelList.clear(); } } @Override public String[] parseHostmask(final String hostmask) { return IRCClientInfo.parseHostFull(hostmask); } @Override public int getMaxTopicLength() { if (h005Info.containsKey(IrcConstants.ISUPPORT_TOPIC_LENGTH)) { try { return Integer.parseInt(h005Info.get(IrcConstants.ISUPPORT_TOPIC_LENGTH)); } catch (NumberFormatException ex) { // Do nothing } } return 0; } @Override public int getMaxLength() { return MAX_LINELENGTH; } @Override public void setCompositionState(final String host, final CompositionState state) { // Do nothing } @Override protected void handleSocketDebug(final String message) { super.handleSocketDebug(message); callDebugInfo(DEBUG_SOCKET, message); } }