// Copyright (c) 2015 Christopher "BlayTheNinth" Baker package net.blay09.mods.eirairc.irc; import net.blay09.mods.eirairc.EiraIRC; import net.blay09.mods.eirairc.api.IRCReplyCodes; import net.blay09.mods.eirairc.api.bot.IRCBot; import net.blay09.mods.eirairc.api.config.IConfigManager; import net.blay09.mods.eirairc.api.irc.IRCChannel; import net.blay09.mods.eirairc.api.irc.IRCConnection; import net.blay09.mods.eirairc.api.irc.IRCMessage; import net.blay09.mods.eirairc.api.irc.IRCUser; import net.blay09.mods.eirairc.bot.IRCBotImpl; import net.blay09.mods.eirairc.config.AuthManager; import net.blay09.mods.eirairc.config.ServerConfig; import net.blay09.mods.eirairc.config.SharedGlobalConfig; import net.blay09.mods.eirairc.config.base.ServiceConfig; import net.blay09.mods.eirairc.config.base.ServiceSettings; import net.blay09.mods.eirairc.handler.IRCEventHandler; import net.blay09.mods.eirairc.util.ConfigHelper; import net.blay09.mods.eirairc.util.Globals; import net.blay09.mods.eirairc.util.Utils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.*; import java.net.*; import java.util.*; public class IRCConnectionImpl implements Runnable, IRCConnection { private static final Logger logger = LogManager.getLogger(); public static class ProxyAuthenticator extends Authenticator { private PasswordAuthentication auth; public ProxyAuthenticator(String username, String password) { auth = new PasswordAuthentication(username, password.toCharArray()); } @Override protected PasswordAuthentication getPasswordAuthentication() { return auth; } } public static final int DEFAULT_PORT = 6667; public static final String CTCP_START = "\u0001"; public static final String CTCP_END = "\u0001"; protected static final int DEFAULT_PROXY_PORT = 1080; private final IRCParser parser = new IRCParser(); protected final IRCSender sender = new IRCSender(this); private final Map<String, IRCChannel> channels = new HashMap<>(); private final Map<String, IRCUser> users = new HashMap<>(); protected final ServerConfig serverConfig; protected final int[] ports; protected final String host; private IRCBotImpl bot; private String nick; private boolean connected; private int waitingReconnect; private int waitingFallbackNick; private boolean silentNickFailure; private String serverType; private String channelTypes = "#&"; private String channelUserModes = "ov"; private String channelUserModePrefixes = "@+"; private boolean isTwitch; private final List<String> joinAfterNickServ = new ArrayList<>(); private boolean waitingOnNickServ; private boolean disableLogger; private Socket socket; protected BufferedWriter writer; protected BufferedReader reader; public IRCConnectionImpl(ServerConfig serverConfig, String nick) { this.serverConfig = serverConfig; this.host = Utils.extractHost(serverConfig.getAddress()); this.isTwitch = (this.host.equals(Globals.TWITCH_SERVER)); this.ports = Utils.extractPorts(serverConfig.getAddress(), DEFAULT_PORT); this.nick = nick; } public void setBot(IRCBotImpl bot) { this.bot = bot; } @Override public String getNick() { return nick; } @Override public IRCUser getBotUser() { return getOrCreateUser(nick); } @Override public boolean isTwitch() { return isTwitch; } @Override public IRCChannel getChannel(String channelName) { return channels.get(channelName.toLowerCase()); } public IRCChannel getOrCreateChannel(String channelName) { IRCChannel channel = getChannel(channelName); if (channel == null) { channel = new IRCChannelImpl(this, channelName); channels.put(channelName.toLowerCase(), channel); } return channel; } @Override public IRCUser getUser(String nick) { return users.get(nick.toLowerCase()); } private IRCUserImpl getOrCreateSender(IRCMessage msg) { if (msg.getPrefixNick() != null) { IRCUserImpl user = (IRCUserImpl) getOrCreateUser(msg.getPrefixNick()); user.setUsername(msg.getPrefixUsername()); user.setHostname(msg.getPrefixHostname()); return user; } return null; } @Override public IRCUser getOrCreateUser(String nick) { IRCUser user = getUser(nick); if (user == null) { user = new IRCUserImpl(this, nick); users.put(nick.toLowerCase(), user); } return user; } @Override public String getHost() { return host; } @Override public Collection<IRCChannel> getChannels() { return channels.values(); } public boolean start() { IRCEventHandler.fireConnectingEvent(this); Thread thread = new Thread(this, "IRC (" + host + ")"); thread.start(); return true; } protected Proxy createProxy() { if (!SharedGlobalConfig.proxyHost.get().isEmpty()) { if (!SharedGlobalConfig.proxyUsername.get().isEmpty() || !SharedGlobalConfig.proxyPassword.get().isEmpty()) { Authenticator.setDefault(new ProxyAuthenticator(SharedGlobalConfig.proxyUsername.get(), SharedGlobalConfig.proxyPassword.get())); } SocketAddress proxyAddr = new InetSocketAddress(Utils.extractHost(SharedGlobalConfig.proxyHost.get()), Utils.extractPorts(SharedGlobalConfig.proxyHost.get(), DEFAULT_PROXY_PORT)[0]); return new Proxy(Proxy.Type.SOCKS, proxyAddr); } return null; } protected Socket connect() throws Exception { for (int i = 0; i < ports.length; i++) { try { SocketAddress targetAddr = new InetSocketAddress(host, ports[i]); Socket newSocket; Proxy proxy = createProxy(); if (proxy != null) { newSocket = new Socket(proxy); } else { newSocket = new Socket(); } if (!SharedGlobalConfig.bindIP.get().isEmpty()) { newSocket.bind(new InetSocketAddress(SharedGlobalConfig.bindIP.get(), ports[i])); } newSocket.connect(targetAddr); writer = new BufferedWriter(new OutputStreamWriter(newSocket.getOutputStream(), serverConfig.getCharset())); reader = new BufferedReader(new InputStreamReader(newSocket.getInputStream(), serverConfig.getCharset())); sender.setWriter(writer); return newSocket; } catch (UnknownHostException e) { throw e; } catch (IOException e) { if (i == ports.length - 1) { throw e; } } } return null; } public void tick() { if (waitingFallbackNick > 0) { waitingFallbackNick--; if (waitingFallbackNick <= 0) { setSilentNickFailure(true); nick(serverConfig.getNick()); } } } @Override public void run() { try { try { socket = connect(); } catch (Exception e) { IRCEventHandler.fireConnectionFailedEvent(this, e); return; } register(); sender.start(); String line; while ((line = reader.readLine()) != null && sender.isRunning()) { if (SharedGlobalConfig.debugMode.get()) { logger.info("< {}", line); } if (!line.isEmpty()) { IRCMessageImpl msg = parser.parse(line); if (!handleNumericMessage(msg)) { handleMessage(msg); } } } } catch (IOException e) { if (!e.getMessage().equals("Socket closed")) { e.printStackTrace(); } else { closeSocket(); } } catch (Exception e) { EiraIRC.proxy.handleException(this, e); closeSocket(); } IRCEventHandler.fireDisconnectEvent(this); if (connected) { tryReconnect(); } } public void tryReconnect() { closeSocket(); if (waitingReconnect == 0) { waitingReconnect = 15000; } else { waitingReconnect *= 2; } IRCEventHandler.fireReconnectEvent(this, waitingReconnect); try { Thread.sleep(waitingReconnect); } catch (InterruptedException e) { e.printStackTrace(); } start(); } @Override public void disconnect(String quitMessage) { connected = false; try { if (writer != null) { if(SharedGlobalConfig.debugMode.get()) { logger.info("> QUIT :{}", quitMessage); } writer.write("QUIT :" + quitMessage + "\r\n"); writer.flush(); } } catch (IOException e) { e.printStackTrace(); } closeSocket(); } private void closeSocket() { try { if (socket != null) { socket.close(); } } catch (IOException e) { e.printStackTrace(); } } private void register() { try { String serverPassword = AuthManager.getServerPassword(getIdentifier()); if (serverPassword != null && !serverPassword.isEmpty()) { if(SharedGlobalConfig.debugMode.get()) { logger.info("> PASS ***************"); } writer.write("PASS " + serverPassword + "\r\n"); } String user = serverConfig.getBotSettings().ident.get() + " \"\" \"\" :" + serverConfig.getBotSettings().description.get(); if(SharedGlobalConfig.debugMode.get()) { logger.info("> NICK {}", nick); logger.info("> USER {}", user); } writer.write("NICK " + nick + "\r\n"); writer.write("USER " + user + "\r\n"); writer.flush(); } catch (IOException e) { e.printStackTrace(); IRCEventHandler.fireConnectionFailedEvent(this, e); if (connected) { tryReconnect(); } } } @Override public void nick(String nick) { if (irc("NICK " + nick)) { this.nick = nick; } waitingFallbackNick = 0; } public void fallbackNick(String nick) { if (irc("NICK " + nick)) { this.nick = nick; } waitingFallbackNick = 3000; } @Override public void join(String channelName, String channelKey) { irc("JOIN " + channelName + (channelKey != null ? (" " + channelKey) : "")); } @Override public void part(String channelName) { if (irc("PART " + channelName)) { IRCChannel channel = channels.remove(channelName.toLowerCase()); if (channel != null) { IRCEventHandler.fireChannelLeftEvent(this, channel); } } } @Override public void mode(String targetName, String flags) { irc("MODE " + targetName + " " + flags); } @Override public void mode(String targetName, String flags, String nick) { irc("MODE " + targetName + " " + flags + " " + nick); } @Override public void topic(String channelName, String topic) { irc("TOPIC " + channelName + " :" + topic); } private boolean handleNumericMessage(IRCMessageImpl msg) { int numeric = msg.getNumericCommand(); if (numeric == -1) { return false; } if (numeric == IRCReplyCodes.RPL_NAMREPLY) { IRCChannelImpl channel = (IRCChannelImpl) getChannel(msg.arg(2)); String[] names = msg.arg(3).split(" "); for (String name : names) { char firstChar = name.charAt(0); int idx = channelUserModePrefixes.indexOf(firstChar); IRCChannelUserMode mode = null; if (idx != -1) { mode = IRCChannelUserMode.fromChar(channelUserModes.charAt(idx)); name = name.substring(1); } IRCUserImpl user = (IRCUserImpl) getOrCreateUser(name); if (mode != null) { user.setChannelUserMode(channel, mode); } user.addChannel(channel); channel.addUser(user); } IRCEventHandler.fireChannelJoinedEvent(this, msg, channel); } else if (numeric == IRCReplyCodes.RPL_WELCOME) { connected = true; waitingReconnect = 0; IRCEventHandler.fireConnectedEvent(this, msg); } else if (numeric == IRCReplyCodes.RPL_TOPIC) { IRCChannelImpl channel = (IRCChannelImpl) getChannel(msg.arg(1)); if (channel != null) { channel.setTopic(msg.arg(2)); IRCEventHandler.fireChannelTopicEvent(this, msg, channel, null, channel.getTopic()); } } else if (numeric == IRCReplyCodes.RPL_WHOISLOGIN) { IRCUserImpl user = (IRCUserImpl) getOrCreateUser(msg.arg(1)); user.setAccountName(msg.arg(2)); } else if (numeric == IRCReplyCodes.RPL_IDENTIFIED || numeric == IRCReplyCodes.RPL_WHOISLOGIN2) { IRCUserImpl user = (IRCUserImpl) getOrCreateUser(msg.arg(1)); user.setAccountName(msg.arg(1)); } else if (numeric == IRCReplyCodes.RPL_ENDOFWHOIS) { IRCUserImpl user = (IRCUserImpl) getOrCreateUser(msg.arg(1)); if (user.getAccountName() == null || user.getAccountName().isEmpty()) { user.setAccountName(null); } } else if (numeric == IRCReplyCodes.RPL_MYINFO) { serverType = msg.arg(1); } else if (numeric == IRCReplyCodes.RPL_ISUPPORT) { for (int i = 0; i < msg.argCount(); i++) { if (msg.arg(i).startsWith("CHANTYPES=")) { channelTypes = msg.arg(i).substring(10); } else if (msg.arg(i).startsWith("PREFIX=")) { String value = msg.arg(i).substring(7); StringBuilder sb = new StringBuilder(); for (int j = 0; j < value.length(); j++) { char c = value.charAt(j); if (c == ')') { channelUserModes = sb.toString(); sb = new StringBuilder(); } else if (c != '(') { sb.append(c); } } channelUserModePrefixes = sb.toString(); } } } else if (numeric == IRCReplyCodes.RPL_MOTD || numeric <= 4 || numeric == 251 || numeric == 252 || numeric == 254 || numeric == 255 || numeric == 265 || numeric == 266 || numeric == 250 || numeric == 375) { if (SharedGlobalConfig.debugMode.get()) { logger.info("Ignoring message code: {} ({} arguments)", msg.getCommand(), msg.argCount()); } } else if (IRCReplyCodes.isErrorCode(numeric)) { IRCEventHandler.fireIRCErrorEvent(this, msg, msg.getNumericCommand(), msg.args()); } else { if (SharedGlobalConfig.debugMode.get()) { logger.warn("Unhandled message code: {} ({} arguments)", msg.getCommand(), msg.argCount()); } } return true; } private boolean handleMessage(IRCMessageImpl msg) { String cmd = msg.getCommand(); if (cmd.equals("PING")) { irc("PONG " + msg.arg(0)); } else if (cmd.equals("PRIVMSG")) { IRCUserImpl user = getOrCreateSender(msg); String target = msg.arg(0); String message = msg.arg(1); boolean isEmote = false; boolean isCTCP = false; if (message.startsWith(CTCP_START)) { message = message.substring(CTCP_START.length(), message.length() - CTCP_END.length()); if (message.startsWith("ACTION ")) { message = message.substring("ACTION ".length()); isEmote = true; // backwards compatibility } else { isCTCP = true; if (channelTypes.indexOf(target.charAt(0)) != -1) { IRCEventHandler.fireChannelCTCPEvent(this, getChannel(target), user, msg, message, false); } else if (target.equals(this.nick)) { IRCEventHandler.firePrivateCTCPEvent(this, user, msg, message, false); } } } if (!isCTCP) { if (channelTypes.indexOf(target.charAt(0)) != -1) { IRCEventHandler.fireChannelChatEvent(this, getChannel(target), user, msg, message, isEmote, false); } else if (target.equals(this.nick)) { IRCEventHandler.firePrivateChatEvent(this, user, msg, message, isEmote, false); } } } else if (cmd.equals("NOTICE")) { IRCUserImpl user = getOrCreateSender(msg); String target = msg.arg(0); String message = msg.arg(1); if (waitingOnNickServ && user != null) { ServiceSettings serviceSettings = ServiceConfig.getSettings(host, serverType); if (serviceSettings.getServiceName().equals(user.getName() + "@" + user.getHostname()) || serviceSettings.getServiceName().equals(user.getName())) { for (String s : joinAfterNickServ) { join(s, null); } joinAfterNickServ.clear(); waitingOnNickServ = false; } } if (message.startsWith(CTCP_START)) { if (channelTypes.indexOf(target.charAt(0)) != -1) { IRCEventHandler.fireChannelCTCPEvent(this, getChannel(target), user, msg, message, true); } else if (target.equals(this.nick) || target.equals("*")) { IRCEventHandler.firePrivateCTCPEvent(this, user, msg, message, true); } } else { if (channelTypes.indexOf(target.charAt(0)) != -1) { IRCEventHandler.fireChannelChatEvent(this, getChannel(target), user, msg, message, false, true); } else if (target.equals(this.nick) || target.equals("*")) { IRCEventHandler.firePrivateChatEvent(this, user, msg, message, false, true); } } } else if (cmd.equals("JOIN")) { IRCUserImpl user = getOrCreateSender(msg); if (user != null) { IRCChannelImpl channel = (IRCChannelImpl) getOrCreateChannel(msg.arg(0)); channel.addUser(user); user.addChannel(channel); IRCEventHandler.fireUserJoinEvent(this, msg, channel, user); } } else if (cmd.equals("PART")) { IRCUserImpl user = getOrCreateSender(msg); if (user != null) { IRCChannelImpl channel = (IRCChannelImpl) getChannel(msg.arg(0)); if (channel != null) { channel.removeUser(user); user.removeChannel(channel); IRCEventHandler.fireUserLeaveEvent(this, msg, channel, user, msg.arg(1)); } } } else if (cmd.equals("TOPIC")) { IRCUser user = getOrCreateSender(msg); IRCChannelImpl channel = (IRCChannelImpl) getChannel(msg.arg(0)); if (channel != null) { channel.setTopic(msg.arg(1)); IRCEventHandler.fireChannelTopicEvent(this, msg, channel, user, channel.getTopic()); } } else if (cmd.equals("NICK")) { IRCUserImpl user = getOrCreateSender(msg); if (user != null) { String newNick = msg.arg(0); users.remove(user.getName().toLowerCase()); String oldNick = user.getName(); user.setName(newNick); users.put(user.getName().toLowerCase(), user); IRCEventHandler.fireNickChangeEvent(this, msg, user, oldNick, newNick); } } else if (cmd.equals("MODE")) { if (channelTypes.indexOf(msg.arg(0).charAt(0)) == -1 || msg.argCount() < 3) { return false; } IRCChannelImpl channel = (IRCChannelImpl) getOrCreateChannel(msg.arg(0)); String mode = msg.arg(1); String param = msg.arg(2); boolean set = false; List<Character> setList = new ArrayList<>(); List<Character> unsetList = new ArrayList<>(); for (int i = 0; i < mode.length(); i++) { char c = mode.charAt(i); if (c == '+') { set = true; } else if (c == '-') { set = false; } else if (set) { setList.add(c); } else { unsetList.add(c); } } IRCUserImpl user = (IRCUserImpl) getOrCreateUser(param); IRCChannelUserMode currentMode = user.getChannelUserMode(channel); for (char c : setList) { int idx = channelUserModes.indexOf(c); if (idx != -1) { user.setChannelUserMode(channel, IRCChannelUserMode.fromChar(c)); } } if (currentMode != null) { for (char c : unsetList) { if (c == currentMode.modeChar) { user.setChannelUserMode(channel, null); } } } } else if (cmd.equals("QUIT")) { IRCUser user = getOrCreateSender(msg); if (user != null) { IRCEventHandler.fireUserQuitEvent(this, msg, user, msg.arg(0)); for (IRCChannel channel : user.getChannels()) { ((IRCChannelImpl) channel).removeUser(user); } users.remove(user.getName().toLowerCase()); } } return false; } public void whois(String nick) { irc("WHOIS " + nick); } public void message(String target, String message) { irc("PRIVMSG " + target + " :" + message); } public void notice(String target, String message) { irc("NOTICE " + target + " :" + message); } @Override public void kick(String channelName, String nick, String reason) { irc("KICK " + channelName + " " + nick + (reason != null ? (" :" + reason) : "")); } @Override public boolean irc(String message) { if(SharedGlobalConfig.debugMode.get() && !disableLogger) { logger.info("> {}", message); } return sender.addToSendQueue(message); } @Override public String getServerType() { return serverType; } @Override public String getChannelTypes() { return channelTypes; } @Override public String getChannelUserModes() { return channelUserModes; } @Override public String getChannelUserModePrefixes() { return channelUserModePrefixes; } @Override public IRCBot getBot() { return bot; } @Override public String getName() { return host; } @Override public ContextType getContextType() { return ContextType.IRCConnection; } @Override public String getIdentifier() { return host; } @Override public IRCConnection getConnection() { return this; } @Override public void message(String message) { } @Override public void notice(String message) { } @Override public void ctcpMessage(String message) { } @Override public void ctcpNotice(String message) { } @Override public IConfigManager getGeneralSettings() { return ConfigHelper.getGeneralSettings(this).manager; } @Override public IConfigManager getBotSettings() { return ConfigHelper.getBotSettings(this).manager; } @Override public IConfigManager getThemeSettings() { return ConfigHelper.getTheme(this).manager; } @Override public int[] getPorts() { return ports; } public ServerConfig getServerConfig() { return serverConfig; } public boolean isConnected() { return connected; } public boolean isSilentNickFailure() { return silentNickFailure; } public void setSilentNickFailure(boolean silentNickFailure) { this.silentNickFailure = silentNickFailure; } public void nickServIdentify() { ServiceSettings settings = ServiceConfig.getSettings(host, serverType); AuthManager.NickServData nickServData = AuthManager.getNickServData(getIdentifier()); if (nickServData != null) { waitingOnNickServ = true; String command = settings.getIdentifyCommand(nickServData.username, nickServData.password); if(SharedGlobalConfig.debugMode.get()) { logger.info(command.replace(nickServData.password, "***************")); } disableLogger = true; irc(command); disableLogger = false; } } public void joinAfterNickServ(String channelName) { joinAfterNickServ.add(channelName); } }