# HG changeset patch # User sheepluva # Date 1622061163 14400 # Node ID 823cf18be1fc2a0fb0bce0d8709743b0d13359b2 # Parent 6409d756e9da8eedce09a2b6d8978386f1f78366 Hedgewars lobby to IRC proxy diff -r 6409d756e9da -r 823cf18be1fc tools/hw2irc/123.45.67.89.auth --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/123.45.67.89.auth Wed May 26 16:32:43 2021 -0400 @@ -0,0 +1,1 @@ +sample_user=5177a58dc65c8a14dc90c69db3bf3dd2 diff -r 6409d756e9da -r 823cf18be1fc tools/hw2irc/net/ercatec/hw/INetClient.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/net/ercatec/hw/INetClient.java Wed May 26 16:32:43 2021 -0400 @@ -0,0 +1,71 @@ +/* + * Java net client for Hedgewars, a free turn based strategy game + * Copyright (c) 2011 Richard Karolyi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +package net.ercatec.hw; + +import java.util.List; + +public interface INetClient +{ + public static enum UserFlagType { UNKNOWN, ADMIN, INROOM, REGISTERED }; + public static enum BanType { BYNICK, BYIP }; + + public void onConnectionLoss(); + public void onDisconnect(String reason); + + public void onMalformedMessage(String contents); + + public String onPasswordHashNeededForAuth(); + + public void onChat(String user, String message); + public void onWelcomeMessage(String message); + + public void onNotice(int number); + public String onNickCollision(String nick); + public void onNickSet(String nick); + + public void onLobbyJoin(String[] users); + public void onLobbyLeave(String user, String reason); + + // TODO flags => enum array? + public void onRoomInfo(String name, String flags, String newName, + int nUsers, int nTeams, String owner, String map, + String style, String scheme, String weapons); + public void onRoomDel(String name); + + public void onRoomJoin(String[] users); + public void onRoomLeave(String[] users); + + public void onPing(); + public void onPong(); + + public void onUserFlagChange(String user, UserFlagType flag, boolean newValue); + + public void onUserInfo(String user, String ip, String version, String room); + + public void onBanListEntry(BanType type, String target, String duration, String reason); + public void onBanListEnd(); + + public void logDebug(String message); + public void logError(String message); + + public void sanitizeInputs(final String[] inputs); +/* + public void onEngineMessage(String message); +*/ +} diff -r 6409d756e9da -r 823cf18be1fc tools/hw2irc/net/ercatec/hw/ProtocolConnection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/net/ercatec/hw/ProtocolConnection.java Wed May 26 16:32:43 2021 -0400 @@ -0,0 +1,649 @@ +/* + * Java net client for Hedgewars, a free turn based strategy game + * Copyright (c) 2011 Richard Karolyi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +package net.ercatec.hw; + +import java.lang.*; +import java.lang.IllegalArgumentException; +import java.lang.Runnable; +import java.lang.Thread; +import java.io.*; +import java.net.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; +import java.util.Vector; +// for auth +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.security.NoSuchAlgorithmException; + +public final class ProtocolConnection implements Runnable +{ + private static final String DEFAULT_HOST = "netserver.hedgewars.org"; + private static final int DEFAULT_PORT = 46631; + private static final String PROTOCOL_VERSION = "53"; + + private final Socket socket; + private BufferedReader fromSvr; + private PrintWriter toSvr; + private final INetClient netClient; + private boolean quit; + private boolean debug; + + private final String host; + private final int port; + + private String nick; + + public ProtocolConnection(INetClient netClient) throws Exception { + this(netClient, DEFAULT_HOST); + } + + public ProtocolConnection(INetClient netClient, String host) throws Exception { + this(netClient, host, DEFAULT_PORT); + } + + public ProtocolConnection(INetClient netClient, String host, int port) throws Exception { + this.netClient = netClient; + this.host = host; + this.port = port; + this.nick = nick = ""; + this.quit = false; + + fromSvr = null; + toSvr = null; + + try { + socket = new Socket(host, port); + fromSvr = new BufferedReader(new InputStreamReader(socket.getInputStream())); + toSvr = new PrintWriter(socket.getOutputStream(), true); + } + catch(Exception ex) { + throw ex; + } + + ProtocolMessage firstMsg = processNextMessage(); + if (firstMsg.getType() != ProtocolMessage.Type.CONNECTED) { + closeConnection(); + throw new Exception("First Message wasn't CONNECTED."); + } + + } + + public void run() { + + try { + while (!quit) { + processNextMessage(); + } + } + catch(Exception ex) { + netClient.logError("FATAL: Run loop died unexpectedly!"); + ex.printStackTrace(); + handleConnectionLoss(); + } + + // only gets here when connection was closed + } + + public void processMessages() { + processMessages(false); + } + + public Thread processMessages(boolean inNewThread) + { + if (inNewThread) + return new Thread(this); + + run(); + return null; + } + + public void processNextClientFlagsMessages() + { + while (!quit) { + if (!processNextMessage(true).isValid) + break; + } + } + + private ProtocolMessage processNextMessage() { + return processNextMessage(false); + } + + private void handleConnectionLoss() { + closeConnection(); + netClient.onConnectionLoss(); + } + + public void close() { + this.closeConnection(); + } + + private synchronized void closeConnection() { + if (quit) + return; + + quit = true; + try { + if (fromSvr != null) + fromSvr.close(); + } catch(Exception ex) {}; + try { + if (toSvr != null) + toSvr.close(); + } catch(Exception ex) {}; + try { + socket.close(); + } catch(Exception ex) {}; + } + + private String resumeLine = ""; + + private ProtocolMessage processNextMessage(boolean onlyIfClientFlags) + { + String line; + final List parts = new ArrayList(32); + + while (!quit) { + + if (!resumeLine.isEmpty()) { + line = resumeLine; + resumeLine = ""; + } + else { + try { + line = fromSvr.readLine(); + + if (onlyIfClientFlags && (parts.size() == 0) + && !line.equals("CLIENT_FLAGS")) { + resumeLine = line; + // return invalid message + return new ProtocolMessage(); + } + } + catch(Exception whoops) { + handleConnectionLoss(); + break; + } + } + + if (line == null) { + handleConnectionLoss(); + // return invalid message + return new ProtocolMessage(); + } + + if (!quit && line.isEmpty()) { + + if (parts.size() > 0) { + + ProtocolMessage msg = new ProtocolMessage(parts); + + netClient.logDebug("Server: " + msg.toString()); + + if (!msg.isValid()) { + netClient.onMalformedMessage(msg.toString()); + if (msg.getType() != ProtocolMessage.Type.BYE) + continue; + } + + final String[] args = msg.getArguments(); + netClient.sanitizeInputs(args); + + + final int argc = args.length; + + try { + switch (msg.getType()) { + + case PING: + netClient.onPing(); + break; + + case LOBBY__JOINED: + try { + assertAuthNotIncomplete(); + } + catch (Exception ex) { + disconnect(); + netClient.onDisconnect(ex.getMessage()); + } + netClient.onLobbyJoin(args); + break; + + case LOBBY__LEFT: + netClient.onLobbyLeave(args[0], args[1]); + break; + + case CLIENT_FLAGS: + String user; + final String flags = args[0]; + if (flags.length() < 2) { + //netClient.onMalformedMessage(msg.toString()); + break; + } + final char mode = flags.charAt(0); + if ((mode != '-') && (mode != '+')) { + //netClient.onMalformedMessage(msg.toString()); + break; + } + + final int l = flags.length(); + + for (int i = 1; i < l; i++) { + // set flag type + final INetClient.UserFlagType flag; + // TODO support more flags + switch (flags.charAt(i)) { + case 'a': + flag = INetClient.UserFlagType.ADMIN; + break; + case 'i': + flag = INetClient.UserFlagType.INROOM; + break; + case 'u': + flag = INetClient.UserFlagType.REGISTERED; + break; + default: + flag = INetClient.UserFlagType.UNKNOWN; + break; + } + + for (int j = 1; j < args.length; j++) { + netClient.onUserFlagChange(args[j], flag, mode=='+'); + } + } + break; + + case CHAT: + netClient.onChat(args[0], args[1]); + break; + + case INFO: + netClient.onUserInfo(args[0], args[1], args[2], args[3]); + break; + + case PONG: + netClient.onPong(); + break; + + case NICK: + final String newNick = args[0]; + if (!newNick.equals(this.nick)) { + this.nick = newNick; + } + netClient.onNickSet(this.nick); + sendMessage(new String[] { "PROTO", PROTOCOL_VERSION }); + break; + + case NOTICE: + // nickname collision + if (args[0].equals("0")) + setNick(netClient.onNickCollision(this.nick)); + break; + + case ASKPASSWORD: + try { + final String pwHash = netClient.onPasswordHashNeededForAuth(); + doAuthPart1(pwHash, args[0]); + } + catch (Exception ex) { + disconnect(); + netClient.onDisconnect(ex.getMessage()); + } + break; + + case ROOMS: + final int nf = ProtocolMessage.ROOM_FIELD_COUNT; + for (int a = 0; a < argc; a += nf) { + handleRoomInfo(args[a+1], Arrays.copyOfRange(args, a, a + nf)); + } + + case ROOM_ADD: + handleRoomInfo(args[1], args); + break; + + case ROOM_DEL: + netClient.onRoomDel(args[0]); + break; + + case ROOM_UPD: + handleRoomInfo(args[0], Arrays.copyOfRange(args, 1, args.length)); + break; + + case BYE: + closeConnection(); + if (argc > 0) + netClient.onDisconnect(args[0]); + else + netClient.onDisconnect(""); + break; + + case SERVER_AUTH: + try { + doAuthPart2(args[0]); + } + catch (Exception ex) { + disconnect(); + netClient.onDisconnect(ex.getMessage()); + } + break; + } + // end of message + return msg; + } + catch(IllegalArgumentException ex) { + + netClient.logError("Illegal arguments! " + + ProtocolMessage.partsToString(parts) + + "caused: " + ex.getMessage()); + + return new ProtocolMessage(); + } + } + } + else + { + parts.add(line); + } + } + + netClient.logError("WARNING: Message wasn't parsed correctly: " + + ProtocolMessage.partsToString(parts)); + // return invalid message + return new ProtocolMessage(); // never to be reached + } + + private void handleRoomInfo(final String name, final String[] info) throws IllegalArgumentException + { + // TODO room flags enum array + + final int nUsers; + final int nTeams; + + try { + nUsers = Integer.parseInt(info[2]); + } + catch(IllegalArgumentException ex) { + throw new IllegalArgumentException( + "Player count is not an valid integer!", + ex); + } + + try { + nTeams = Integer.parseInt(info[3]); + } + catch(IllegalArgumentException ex) { + throw new IllegalArgumentException( + "Team count is not an valid integer!", + ex); + } + + netClient.onRoomInfo(name, info[0], info[1], nUsers, nTeams, + info[4], info[5], info[6], info[7], info[8]); + } + + private static final String AUTH_SALT = PROTOCOL_VERSION + "!hedgewars"; + private static final int PASSWORD_HASH_LENGTH = 32; + public static final int SERVER_SALT_MIN_LENGTH = 16; + private static final String AUTH_ALG = "SHA-1"; + private String serverAuthHash = ""; + + private void assertAuthNotIncomplete() throws Exception { + if (!serverAuthHash.isEmpty()) { + netClient.logError("AUTH-ERROR: assertAuthNotIncomplete() found that authentication was not completed!"); + throw new Exception("Authentication was not finished properly!"); + } + serverAuthHash = ""; + } + + private void doAuthPart2(final String serverAuthHash) throws Exception { + if (!this.serverAuthHash.equals(serverAuthHash)) { + netClient.logError("AUTH-ERROR: Server's authentication hash is incorrect!"); + throw new Exception("Server failed mutual authentication! (wrong hash provided by server)"); + } + netClient.logDebug("Auth: Mutual authentication successful."); + this.serverAuthHash = ""; + } + + private void doAuthPart1(final String pwHash, final String serverSalt) throws Exception { + if ((pwHash == null) || pwHash.isEmpty()) { + netClient.logDebug("AUTH: Password required, but no password hash was provided."); + throw new Exception("Auth: Password needed, but none specified."); + } + if (pwHash.length() != PASSWORD_HASH_LENGTH) { + netClient.logError("AUTH-ERROR: Your password hash has an unexpected length! Should be " + + PASSWORD_HASH_LENGTH + " but is " + pwHash.length() + ); + throw new Exception("Auth: Your password hash length seems wrong."); + } + if (serverSalt.length() < SERVER_SALT_MIN_LENGTH) { + netClient.logError("AUTH-ERROR: Salt provided by server is too short! Should be at least " + + SERVER_SALT_MIN_LENGTH + " but is " + serverSalt.length() + ); + throw new Exception("Auth: Server violated authentication protocol! (auth salt too short)"); + } + + final MessageDigest sha1Digest; + + try { + sha1Digest = MessageDigest.getInstance(AUTH_ALG); + } + catch(NoSuchAlgorithmException ex) { + netClient.logError("AUTH-ERROR: Algorithm required for authentication (" + + AUTH_ALG + ") not available!" + ); + return; + } + + + // generate 130 bit base32 encoded value + // base32 = 5bits/char => 26 chars, which is more than min req + final String clientSalt = + new BigInteger(130, new SecureRandom()).toString(32); + + final String saltedPwHash = + clientSalt + serverSalt + pwHash + AUTH_SALT; + + final String saltedPwHash2 = + serverSalt + clientSalt + pwHash + AUTH_SALT; + + final String clientAuthHash = + new BigInteger(1, sha1Digest.digest(saltedPwHash.getBytes("UTF-8"))).toString(16); + + serverAuthHash = + new BigInteger(1, sha1Digest.digest(saltedPwHash2.getBytes("UTF-8"))).toString(16); + + sendMessage(new String[] { "PASSWORD", clientAuthHash, clientSalt }); + +/* When we got password hash, and server asked us for a password, perform mutual authentication: + * at this point we have salt chosen by server + * client sends client salt and hash of secret (password hash) salted with client salt, server salt, + * and static salt (predefined string + protocol number) + * server should respond with hash of the same set in different order. + + if(m_passwordHash.isEmpty() || m_serverSalt.isEmpty()) + return; + + QString hash = QCryptographicHash::hash( + m_clientSalt.toAscii() + .append(m_serverSalt.toAscii()) + .append(m_passwordHash) + .append(cProtoVer->toAscii()) + .append("!hedgewars") + , QCryptographicHash::Sha1).toHex(); + + m_serverHash = QCryptographicHash::hash( + m_serverSalt.toAscii() + .append(m_clientSalt.toAscii()) + .append(m_passwordHash) + .append(cProtoVer->toAscii()) + .append("!hedgewars") + , QCryptographicHash::Sha1).toHex(); + + RawSendNet(QString("PASSWORD%1%2%1%3").arg(delimiter).arg(hash).arg(m_clientSalt)); + +Server: ("ASKPASSWORD", "5S4q9Dd0Qrn1PNsxymtRhupN") +Client: ("PASSWORD", "297a2b2f8ef83bcead4056b4df9313c27bb948af", "{cc82f4ca-f73c-469d-9ab7-9661bffeabd1}") +Server: ("SERVER_AUTH", "06ecc1cc23b2c9ebd177a110b149b945523752ae") + + */ + } + + public void sendCommand(final String command) + { + String cmd = command; + + // don't execute empty commands + if (cmd.length() < 1) + return; + + // replace all newlines since they violate protocol + cmd = cmd.replace('\n', ' '); + + // parameters are separated by one or more spaces. + final String[] parts = cmd.split(" +"); + + // command is always CAPS + parts[0] = parts[0].toUpperCase(); + + sendMessage(parts); + } + + public void sendPing() + { + sendMessage("PING"); + } + + public void sendPong() + { + sendMessage("PONG"); + } + + private void sendMessage(final String msg) + { + sendMessage(new String[] { msg }); + } + + private void sendMessage(final String[] parts) + { + if (quit) + return; + + netClient.logDebug("Client: " + messagePartsToString(parts)); + + boolean malformed = false; + String msg = ""; + + for (final String part : parts) + { + msg += part + '\n'; + if (part.isEmpty() || (part.indexOf('\n') >= 0)) { + malformed = true; + break; + } + } + + if (malformed) { + netClient.onMalformedMessage(messagePartsToString(parts)); + return; + } + + try { + toSvr.print(msg + '\n'); // don't use println, since we always want '\n' + toSvr.flush(); + } + catch(Exception ex) { + netClient.logError("FATAL: Couldn't send message! " + ex.getMessage()); + ex.printStackTrace(); + handleConnectionLoss(); + } + } + + private String messagePartsToString(String[] parts) { + + if (parts.length == 0) + return "([empty message])"; + + String result = "(\"" + parts[0] + '"'; + for (int i=1; i < parts.length; i++) + { + result += ", \"" + parts[i] + '"'; + } + result += ')'; + + return result; + } + + public void disconnect() { + sendMessage(new String[] { "QUIT", "Client quit" }); + closeConnection(); + } + + public void disconnect(final String reason) { + sendMessage(new String[] { "QUIT", reason.isEmpty()?"-":reason }); + closeConnection(); + } + + public void sendChat(String message) { + + String[] lines = message.split("\n"); + + for (String line : lines) + { + if (!message.trim().isEmpty()) + sendMessage(new String[] { "CHAT", line }); + } + } + + public void joinRoom(final String roomName) { + + sendMessage(new String[] { "JOIN_ROOM", roomName }); + } + + public void leaveRoom(final String roomName) { + + sendMessage("PART"); + } + + public void requestInfo(final String user) { + + sendMessage(new String[] { "INFO", user }); + } + + public void setNick(final String nick) { + + this.nick = nick; + sendMessage(new String[] { "NICK", nick }); + } + + public void kick(final String nick) { + + sendMessage(new String[] { "KICK", nick }); + } + + public void requestRoomsList() { + + sendMessage("LIST"); + } +} + diff -r 6409d756e9da -r 823cf18be1fc tools/hw2irc/net/ercatec/hw/ProtocolMessage.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/net/ercatec/hw/ProtocolMessage.java Wed May 26 16:32:43 2021 -0400 @@ -0,0 +1,280 @@ +/* + * Java net client for Hedgewars, a free turn based strategy game + * Copyright (c) 2011 Richard Karolyi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + */ + +package net.ercatec.hw; + +import java.util.Arrays; +import java.util.List; +import java.util.Iterator; + +public final class ProtocolMessage +{ + public static final int ROOM_FIELD_COUNT = 9; + + private static int minServerVersion = 49; + + public static enum Type { + // for unknown message types + _UNKNOWN_MESSAGETYPE_, + // server messages + ERROR, + PING, + PONG, + NICK, + PROTO, + ASKPASSWORD, + SERVER_AUTH, + CONNECTED, + SERVER_MESSAGE, + BYE, + INFO, + NOTICE, + CHAT, + LOBBY__JOINED, + LOBBY__LEFT, + ROOMS, + ROOM, + ROOM_ADD, + ROOM_DEL, + ROOM_UPD, + ROOM__JOINED, + ROOM__LEFT, + CFG, + TOGGLE_RESTRICT_TEAMS, + CLIENT_FLAGS, + CF, // this just an alias and will be mapped to CLIENT_FLAGS + EM // engine messages + } + + public final boolean isValid; + private Type type; + private String[] args; + +/* + public ProtocolMessage(String messageType) + { + args = new String[0]; + + try + { + type = Type.valueOf(messageType); + isValid = messageSyntaxIsValid(); + } + catch (IllegalArgumentException whoops) + { + type = Type._UNKNOWN_MESSAGETYPE_; + args = new String[] { messageType }; + isValid = false; + } + } +*/ + + private final String[] emptyArgs = new String[0]; + + private String[] withoutFirst(final String[] array, final int amount) { + return Arrays.copyOfRange(array, amount, array.length); + } + + private final List parts; + + // invalid Message + public ProtocolMessage() { + this.parts = Arrays.asList(emptyArgs); + this.args = emptyArgs; + this.isValid = false; + } + + public ProtocolMessage(final List parts) + { + this.parts = parts; + this.args = emptyArgs; + + final int partc = parts.size(); + + if (partc < 1) { + isValid = false; + return; + } + + try { + type = Type.valueOf(parts.get(0).replaceAll(":", "__")); + } + catch (IllegalArgumentException whoops) { + type = Type._UNKNOWN_MESSAGETYPE_; + } + + if (type == Type._UNKNOWN_MESSAGETYPE_) { + args = parts.toArray(args); + isValid = false; + } + else { + // all parts after command become arguments + if (partc > 1) + args = withoutFirst(parts.toArray(args), 1); + isValid = checkMessage(); + } + } + + private boolean checkMessage() + { + int argc = args.length; + + switch (type) + { + // no arguments allowed + case PING: + case PONG: + case TOGGLE_RESTRICT_TEAMS: + if (argc != 0) + return false; + break; + + // one argument or more + case EM: // engine messages + case LOBBY__JOINED: // list of joined players + case ROOM__JOINED: // list of joined players + if (argc < 1) + return false; + break; + + // one argument + case SERVER_MESSAGE: + case BYE: // disconnect reason + case ERROR: // error message + case NICK: // nickname + case PROTO: // protocol version + case SERVER_AUTH: // last stage of mutual of authentication + case ASKPASSWORD: // request for auth with salt + if (argc != 1) + return false; + break; + + case NOTICE: // argument should be a number + if (argc != 1) + return false; + try { + Integer.parseInt(args[0]); + } + catch (NumberFormatException e) { + return false; + } + break; + + // two arguments + case CONNECTED: // server description and version + case CHAT: // player nick and chat message + case LOBBY__LEFT: // player nick and leave reason + case ROOM__LEFT: // player nick and leave reason + if (argc != 2) + return false; + break; + + case ROOM: // "ADD" (or "UPD" + room name ) + room attrs or "DEL" and room name + if(argc < 2) + return false; + + final String subC = args[0]; + + if (subC.equals("ADD")) { + if(argc != ROOM_FIELD_COUNT + 1) + return false; + this.type = Type.ROOM_ADD; + this.args = withoutFirst(args, 1); + } + else if (subC.equals("UPD")) { + if(argc != ROOM_FIELD_COUNT + 2) + return false; + this.type = Type.ROOM_UPD; + this.args = withoutFirst(args, 1); + } + else if (subC.equals("DEL") && (argc == 2)) { + this.type = Type.ROOM_DEL; + this.args = withoutFirst(args, 1); + } + else + return false; + break; + + // two arguments or more + case CFG: // setting name and list of setting parameters + if (argc < 2) + return false; + break; + case CLIENT_FLAGS: // string of changed flags and player name(s) + case CF: // alias of CLIENT_FLAGS + if (argc < 2) + return false; + if (this.type == Type.CF) + this.type = Type.CLIENT_FLAGS; + break; + + // four arguments + case INFO: // info about a player, name, ip/id, version, room + if (argc != 4) + return false; + break; + + // multiple of ROOM_FIELD_COUNT arguments (incl. 0) + case ROOMS: + if (argc % ROOM_FIELD_COUNT != 0) + return false; + break; + } + + return true; + } + + private void maybeSendPassword() { + + } + + public Type getType() + { + return type; + } + + public String[] getArguments() + { + return args; + } + + public boolean isValid() + { + return isValid; + } + + public static String partsToString(final List parts) + { + final Iterator iter = parts.iterator(); + + if (!iter.hasNext()) + return "( -EMPTY- )"; + + String result = "(\"" + iter.next(); + + while (iter.hasNext()) { + result += "\", \"" + iter.next(); + } + + return result + "\")"; + } + + public String toString() { + return partsToString(this.parts); + } +} diff -r 6409d756e9da -r 823cf18be1fc tools/hw2irc/net/ercatec/hw2ircsvr/Connection.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/net/ercatec/hw2ircsvr/Connection.java Wed May 26 16:32:43 2021 -0400 @@ -0,0 +1,1814 @@ +package net.ercatec.hw2ircsvr; + +import net.ercatec.hw.INetClient; +import net.ercatec.hw.ProtocolConnection; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.Collections; +import java.util.Vector; + +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import java.lang.IllegalArgumentException; + +// for auth files +import java.util.Properties; +import java.io.FileInputStream; +import java.io.IOException; + +/* TODO + * disconnect clients that are not irc clients + * disconnect excess flooders + * recognizes stuff after : as single arg + * collect pre-irc-join messages and show on join + * allow negating regexps + * ban + * banlist + * commandquery // wth did I mean by that? + * more room info + * filter rooms + * warnings + * global notice + */ + +/** + * @author sheepluva + * + * based on jircs by Alexander Boyd + */ +public class Connection implements INetClient, Runnable +{ + private static final String DESCRIPTION_SHORT + = "connect to hedgewars via irc!"; + + private static final String VERSION = "0.6.7-Alpha_2015-11-07"; + + + private static final String MAGIC_BYTES = "[\1\2\3]"; + private static final char MAGIC_BYTE_ACTION = ((char)1); // ^A + private static final char MAGIC_BYTE_BOLD = ((char)2); // ^B + private static final char MAGIC_BYTE_COLOR = ((char)3); // ^C + + private static final String[] DEFAULT_MOTD = { + " ", + " "+MAGIC_BYTE_COLOR+"06"+MAGIC_BYTE_BOLD+" SUCH FLUFFY!", + " ", + " "+MAGIC_BYTE_COLOR+"04 MUCH BAH "+MAGIC_BYTE_COLOR+"00__ _ ", + " "+MAGIC_BYTE_COLOR+"00 .-.' `; `-."+MAGIC_BYTE_COLOR+"00_ __ _ ", + " "+MAGIC_BYTE_COLOR+"00 (_, .-:' `; `"+MAGIC_BYTE_COLOR+"00-._ ", + " "+MAGIC_BYTE_COLOR+"14 ,'"+MAGIC_BYTE_COLOR+"02o "+MAGIC_BYTE_COLOR+"00( (_, ) ", + " "+MAGIC_BYTE_COLOR+"14 (__"+MAGIC_BYTE_COLOR+"00,-' "+MAGIC_BYTE_COLOR+"15,'"+MAGIC_BYTE_COLOR+"12o "+MAGIC_BYTE_COLOR+"00( )> ", + " "+MAGIC_BYTE_COLOR+"00 ( "+MAGIC_BYTE_COLOR+"15(__"+MAGIC_BYTE_COLOR+"00,-' ) ", + " "+MAGIC_BYTE_COLOR+"00 `-'._.--._( ) ", + " "+MAGIC_BYTE_COLOR+"14 ||| |||"+MAGIC_BYTE_COLOR+"00`-'._.--._.-' ", + " "+MAGIC_BYTE_COLOR+"15 ||| ||| ", + " "+MAGIC_BYTE_COLOR+"07"+MAGIC_BYTE_BOLD+" WOW! ", + " "+MAGIC_BYTE_COLOR+"09 VERY SHEEP ", + " ", + " ", + " ", + " "+MAGIC_BYTE_COLOR+"4 Latest hw2irc crimes/changes:", + " ping: ping of hwserver will only get reply if irc client pingable", + " ping: pings of irc clients will only get reply if hwserver pingable", + " rooms: id-rotation, make channel names at least 2 digits wide", + " auth: support passhash being loaded local auth file and irc pass (sent as cleartext - DO NOT USE!)", + " ", + " ", + }; + + + private static final String DEFAULT_QUIT_REASON = "User quit"; + // NOT final + private static char CHAT_COMMAND_CHAR = '\\'; + + private final class Room { + public final int id; + public final String chan; + public String name; + private String owner = ""; + public int nPlayers = 0; + public int nTeams = 0; + + public Room(final int id, final String name, final String owner) { + this.id = id; + this.chan = (id<10?"#0":"#") + id; + this.name = name; + this.setOwner(owner); + } + + public String getOwner() { return this.owner; } + + public void setOwner(final String owner) { + // don't to this for first owner + if (!this.owner.isEmpty()) { + + // owner didn't change + if (this.owner.equals(owner)) + return; + + // update old room owner + final Player oldOwner = allPlayers.get(this.owner); + + if (oldOwner != null) + oldOwner.isRoomAdm = false; + + } + + // update new room owner + final Player newOwner = allPlayers.get(owner); + + if (newOwner != null) + newOwner.isRoomAdm = true; + + this.owner = owner; + + } + } + + private final class Player { + public final String nick; + public final String ircNick; + private boolean isAdm; + private boolean isCont; + private boolean isReg; + public boolean inRoom; + public boolean isRoomAdm; + private String ircId; + private String ircHostname; + private boolean announced; + + // server info + private String version = ""; + private String ip = ""; + private String room = ""; + + public Player(final String nick) { + this.nick = nick; + this.ircNick = hwToIrcNick(nick); + this.announced = false; + updateIrcHostname(); + } + + public String getIrcHostname() { return ircHostname; } + public String getIrcId() { return ircId; } + + public String getRoom() { + if (room.isEmpty()) + return room; + + return "[" + ((isAdm?"@":"") + (isRoomAdm?"+":"") + this.room); + } + + public boolean needsAnnounce() { + return !announced; + } + + public void setAnnounced() { + announced = true; + } + + public void setInfo(final String ip, final String version, final String room) { + if (this.version.isEmpty()) { + this.version = version; + this.ip = ip.replaceAll("^\\[|]$", ""); + updateIrcHostname(); + } + + if (room.isEmpty()) + this.room = room; + else + this.room = room.replaceAll("^\\[[@+]*", ""); + } + + public boolean isServerAdmin() { return isAdm; } + //public boolean isContributor() { return isCont; } + public boolean isRegistered() { return isReg; } + + public void setServerAdmin(boolean isAdm) { + this.isAdm = isAdm; updateIrcHostname(); } + public void setContributor(boolean isCont) { + this.isCont = isCont; updateIrcHostname(); } + public void setRegistered(boolean isReg) { + this.isReg = isReg; updateIrcHostname(); } + + private void updateIrcHostname() { + ircHostname = ip.isEmpty()?"":(ip + '/'); + ircHostname += "hw/"; + if (!version.isEmpty()) + ircHostname += version; + if (isAdm) + ircHostname += "/admin"; + else if (isCont) + ircHostname += "/contributor"; + else if (isReg) + ircHostname += "/member"; + else + ircHostname += "/player"; + + updateIrcId(); + } + + private void updateIrcId() { + ircId = ircNick + "!~" + ircNick + "@" + ircHostname; + } + } + + public String hw404NickToIrcId(String nick) { + nick = hwToIrcNick(nick); + return nick + "!~" + nick + "@hw/404"; + } + + // hash tables are thread-safe + private final Map allPlayers = new Hashtable(); + private final Map roomPlayers = new Hashtable(); + private final Map roomsById = new Hashtable(); + private final Map roomsByName = new Hashtable(); + private final List roomsSorted = new Vector(); + + private final List ircPingQueue = new Vector(); + + private static final String DEFAULT_SERVER_HOST = "netserver.hedgewars.org"; + private static String SERVER_HOST = DEFAULT_SERVER_HOST; + private static int IRC_PORT = 46667; + + private String hostname; + + private static final String LOBBY_CHANNEL_NAME = "#lobby"; + private static final String ROOM_CHANNEL_NAME = "#room"; + + // hack + // TODO: , + private static final char MAGIC_SPACE = ' '; + private static final char MAGIC_ATSIGN = '៙'; + private static final char MAGIC_PERCENT = '%'; + private static final char MAGIC_PLUS = '+'; + private static final char MAGIC_EXCLAM = '❢'; + private static final char MAGIC_COMMA = ','; + private static final char MAGIC_COLON = ':'; + + private static String hwToIrcNick(final String nick) { + return nick + .replace(' ', MAGIC_SPACE) + .replace('@', MAGIC_ATSIGN) + .replace('%', MAGIC_PERCENT) + .replace('+', MAGIC_PLUS) + .replace('!', MAGIC_EXCLAM) + .replace(',', MAGIC_COMMA) + .replace(':', MAGIC_COLON) + ; + } + private static String ircToHwNick(final String nick) { + return nick + .replace(MAGIC_COLON, ':') + .replace(MAGIC_COMMA, ',') + .replace(MAGIC_EXCLAM, '!') + .replace(MAGIC_PLUS, '+') + .replace(MAGIC_PERCENT, '%') + .replace(MAGIC_ATSIGN, '@') + .replace(MAGIC_SPACE, ' ') + ; + } + + private ProtocolConnection hwcon; + private boolean joined = false; + private boolean ircJoined = false; + + private void collectFurtherInfo() { + hwcon.sendPing(); + hwcon.processNextClientFlagsMessages(); + } + + public void onPing() { + send("PING :" + globalServerName); + } + + public void onPong() { + if (!ircPingQueue.isEmpty()) + send(":" + globalServerName + " PONG " + globalServerName + + " :" + ircPingQueue.remove(0)); + + } + + public void onConnectionLoss() { + quit("Connection Loss"); + } + + public void onDisconnect(final String reason) { + quit(reason); + } + + public String onPasswordHashNeededForAuth() { + return passwordHash; + } + + public void onMalformedMessage(String contents) + { + this.logError("MALFORMED MESSAGE: " + contents); + } + + public void onChat(final String user, final String message) { + String ircId; + Player player = allPlayers.get(user); + if (player == null) { + // fake user - so probably a notice + sendChannelNotice(message, hwToIrcNick(user)); + //logWarning("onChat(): Couldn't find player with specified nick! nick: " + user); + //send(":" + hw404NickToIrcId(user) + " PRIVMSG " + //+ LOBBY_CHANNEL_NAME + " :" + hwActionToIrc(message)); + } + else + send(":" + player.getIrcId() + " PRIVMSG " + + LOBBY_CHANNEL_NAME + " :" + hwActionToIrc(message)); + } + + public void onWelcomeMessage(final String message) { + } + + public void onNotice(int number) { + } + + public void onBanListEntry(BanType type, String target, String duration, String reason) { + // TODO + } + public void onBanListEnd() { + // TODO + } + + public String onNickCollision(final String nick) { + return nick + "_"; + } + + public void onNickSet(final String nick) { + final String newNick = hwToIrcNick(nick); + // tell irc client + send(":" + ownIrcNick + "!~" + username + "@" + + hostname + " NICK :" + nick); + ownIrcNick = newNick; + updateLogPrefix(); + logInfo("Nickname set to " + nick); + } + + private void flagAsInLobby(final Player player) { + if (!ircJoined) + return; + final String ircNick = player.ircNick; + if (player.isServerAdmin()) + send(":room-part!~@~ MODE " + LOBBY_CHANNEL_NAME + " -h+o " + ircNick + " " + ircNick); + //else + //send(":room-part!~@~ MODE " + LOBBY_CHANNEL_NAME + " +v " + ircNick); + } + + private void flagAsInRoom(final Player player) { + if (!ircJoined) + return; + final String ircNick = player.ircNick; + if (player.isServerAdmin()) + send(":room-join!~@~ MODE " + LOBBY_CHANNEL_NAME + " -o+h " + ircNick + " " + ircNick); + //else + //send(":room-join!~@~ MODE " + LOBBY_CHANNEL_NAME + " -v " + ircNick); + } + +// TODO somewhere: escape char for magic chars! + +// TODO /join with playername => FOLLOW :D + + public void sendPlayerMode(final Player player) { + char c; + if (player.isServerAdmin()) + c = player.inRoom?'h':'o'; + else if (player.isRegistered()) + c = 'v'; + else + // no mode + return; + + send(":server-join!~@~ MODE " + LOBBY_CHANNEL_NAME + " +" + c + " " + player.ircNick); + } + + private Player ownPlayer = null; + + public void onLobbyJoin(final String[] users) { + + final List newPlayers = new ArrayList(users.length); + + // process joins + for (final String user : users) { + final Player player = new Player(user); + if (ownPlayer == null) + ownPlayer = player; + newPlayers.add(player); + allPlayers.put(user, player); + } + + // make sure we get the client flags before we announce anything + collectFurtherInfo(); + + // get player info + // NOTE: if player is in room, then info was already retrieved + for (final Player player : newPlayers) { + if (!player.inRoom) + hwcon.requestInfo(player.nick); + } + + /* DISABLED - we'll announce later - when receiving info + // announce joins + if (ircJoined) { + for (final Player player : newPlayers) { + final String ircId = player.getIrcId(); + send(":" + ircId + + " JOIN "+ lobbyChannel.name); + sendPlayerMode(player); + } + } + */ + if (!ircJoined) { + // don't announced players that were there before join already + for (final Player player : newPlayers) { + player.setAnnounced(); + } + } + + if (!joined) { + joined = true; + // forget password hash, we don't need it anymore. + passwordHash = ""; + logInfo("Hedgewars server/lobby joined."); + sendSelfNotice("Hedgewars server was joined successfully"); + // do this after join so that rooms can be assigned to their owners + hwcon.requestRoomsList(); + } + } + + private void makeIrcJoinLobby() { + sendGlobal("INVITE " + ownIrcNick + " " + LOBBY_CHANNEL_NAME); + try{Thread.sleep(3000);}catch(Exception e){} + join(lobbyChannel.name); + sendSelfNotice("Joining lobby-channel: " + lobbyChannel.name); + } + + private void announcePlayerJoinLobby(final Player player) { + player.setAnnounced(); + send(":" + player.getIrcId() + + " JOIN "+ lobbyChannel.name); + sendPlayerMode(player); + } + + public void onLobbyLeave(final String user, final String reason) { + final Player player = allPlayers.get(user); + if (player == null) { + logWarning("onLobbyLeave(): Couldn't find player with specified nick! nick: " + user); + sendIfJoined(":" + hw404NickToIrcId(user) + + " PART " + lobbyChannel.name + " " + reason); + } + else { + if (ircJoined && player.needsAnnounce()) + announcePlayerJoinLobby(player); + sendIfJoined(":" + player.getIrcId() + + " PART " + lobbyChannel.name + " " + reason); + allPlayers.remove(user); + } + } + + private int lastRoomId = 0; + + public void onRoomInfo(final String name, final String flags, + final String newName, final int nUsers, + final int nTeams, final String owner, + final String map, final String style, + final String scheme, final String weapons) { + + Room room = roomsByName.get(name); + + if (room == null) { + // try to reuse old ids + if (lastRoomId >= 90) + lastRoomId = 9; + + // search for first free + while(roomsById.containsKey(++lastRoomId)) { } + + room = new Room(lastRoomId, newName, owner); + roomsById.put(lastRoomId, room); + roomsByName.put(newName, room); + roomsSorted.add(room); + } + else if (!room.name.equals(newName)) { + room.name = newName; + roomsByName.put(newName, roomsByName.remove(name)); + } + + // update data + room.setOwner(owner); + room.nPlayers = nUsers; + room.nTeams = nTeams; + } + + public void onRoomDel(final String name) { + final Room room = roomsByName.remove(name); + + if (room != null) { + roomsById.remove(room.id); + roomsSorted.remove(room); + } + } + + public void onRoomJoin(final String[] users) { + } + + public void onRoomLeave(final String[] users) { + } + + // TODO vector that remembers who's info was requested for manually + List requestedInfos = new Vector(); + + public void onUserInfo(final String user, final String ip, final String version, final String room) { + Player player = allPlayers.get(user); + if (player != null) { + player.setInfo(ip, version, room); + if (ircJoined) { + if (player.needsAnnounce()) + announcePlayerJoinLobby(player); + } + else { + if (player == ownPlayer) { + + makeIrcJoinLobby(); + } + } + } + + // if MANUAL send notice + if (requestedInfos.remove(user)) { + final String nick = hwToIrcNick(user); + sendServerNotice(nick + " - " + buildInfoString(ip, version, room)); + } + } + + public void onUserFlagChange(final String user, final UserFlagType flag, final boolean newValue) { + final Player player = allPlayers.get(user); + if (player == null) { + logError("onUserFlagChange(): Couldn't find player with specified nick! nick: " + user); + return; + } + switch (flag) { + case ADMIN: + player.setServerAdmin(newValue); + if (newValue) { + logDebug(user + " is server admin"); + //sendIfJoined(":server!~@~ MODE " + LOBBY_CHANNEL_NAME + " -v+o " + player.ircNick + " " + player.ircNick); + } + break; + case INROOM: + player.inRoom = newValue; + if (newValue) { + flagAsInRoom(player); + logDebug(user + " entered a room"); + // get new room info + hwcon.requestInfo(player.nick); + } + else { + flagAsInLobby(player); + logDebug(user + " returned to lobby"); + player.inRoom = false; + } + break; + case REGISTERED: + player.setRegistered(newValue); + break; + default: break; + } + } + + public class Channel + { + private String topic; + private final String name; + private final Map players; + + public Channel(final String name, final String topic, final Map players) { + this.name = name; + this.topic = topic; + this.players = players; + } + } + + public void logInfo(final String message) { + System.out.println(this.logPrefix + ": " + message); + } + + public void logDebug(final String message) { + System.out.println(this.logPrefix + "| " + message); + } + + public void logWarning(final String message) { + System.err.println(this.logPrefix + "? " + message); + } + + public void logError(final String message) { + System.err.println(this.logPrefix + "! " + message); + } + + + //private static final Object mutex = new Object(); + private boolean joinSent = false; + private Socket socket; + private String username; + private String ownIrcNick; + private String description; + private static Map connectionMap = new HashMap(); + // TODO those MUST NOT be static! + //private Map channelMap = new HashMap(); + private final Channel lobbyChannel; + private static String globalServerName; + private String logPrefix; + private final String clientId; + private String passwordHash = ""; + + private final Connection thisConnection; + + public Connection(Socket socket, final String clientId) throws Exception + { + this.ownIrcNick = "NONAME"; + this.socket = socket; + this.hostname = ((InetSocketAddress)socket.getRemoteSocketAddress()) + .getAddress().getHostAddress(); + this.clientId = clientId; + updateLogPrefix(); + thisConnection = this; + logInfo("New Connection"); + + this.hwcon = null; + + try { + this.hwcon = new ProtocolConnection(this, SERVER_HOST); + logInfo("Connection to " + SERVER_HOST + " established."); + } + catch(Exception ex) { + final String errmsg = "Could not connect to " + SERVER_HOST + ": " + + ex.getMessage(); + logError(errmsg); + sendQuit(errmsg); + } + + final String lobbyTopic = " # " + SERVER_HOST + " - HEDGEWARS SERVER LOBBY # "; + this.lobbyChannel = new Channel(LOBBY_CHANNEL_NAME, lobbyTopic, allPlayers); + + // start in new thread + if (hwcon != null) { + (this.hwcon.processMessages(true)).start(); + } + } + + private void updateLogPrefix() { + if (ownIrcNick == null) + this.logPrefix = clientId + " "; + else + this.logPrefix = clientId + " [" + ownIrcNick + "] "; + } + + private void setNick(final String nick) { + if (passwordHash.isEmpty()) { + try { + final Properties authProps = new Properties(); + final String authFile = this.hostname + ".auth"; + logInfo("Attempting to load auth info from " + authFile); + authProps.load(new FileInputStream(authFile)); + passwordHash = authProps.getProperty(nick, ""); + if (passwordHash.isEmpty()) + logInfo("Auth info file didn't contain any password hash for: " + nick); + } catch (IOException e) { + logInfo("Auth info file couldn't be loaded."); + } + } + + // append _ just in case + if (!passwordHash.isEmpty() || nick.endsWith("_")) { + ownIrcNick = nick; + hwcon.setNick(ircToHwNick(nick)); + } + else { + final String nick_ = nick + "_"; + ownIrcNick = nick_; + hwcon.setNick(ircToHwNick(nick_)); + } + } + + public String getRepresentation() + { + return ownIrcNick + "!~" + username + "@" + hostname; + } + + private static int lastClientId = 0; + + /** + * @param args + */ + public static void main(String[] args) throws Throwable + { + if (args.length > 0) + { + SERVER_HOST = args[0]; + } + if (args.length > 1) + { + IRC_PORT = Integer.parseInt(args[1]); + } + + globalServerName = "hw2irc"; + + if (!SERVER_HOST.equals(DEFAULT_SERVER_HOST)) + globalServerName += "~" + SERVER_HOST; + + final int port = IRC_PORT; + ServerSocket ss = new ServerSocket(port); + System.out.println("Listening on port " + port); + while (true) + { + Socket s = ss.accept(); + final String clientId = "client" + (++lastClientId) + '-' + + ((InetSocketAddress)s.getRemoteSocketAddress()) + .getAddress().getHostAddress(); + try { + Connection clientCon = new Connection(s, clientId); + //clientCon.run(); + Thread clientThread = new Thread(clientCon, clientId); + clientThread.start(); + } + catch (Exception ex) { + System.err.println("FATAL: Server connection thread " + clientId + " crashed on startup! " + ex.getMessage()); + ex.printStackTrace(); + } + + System.out.println("Note: Not accepting new clients for the next " + SLEEP_BETWEEN_LOGIN_DURATION + "s, trying to avoid reconnecting too quickly."); + Thread.sleep(SLEEP_BETWEEN_LOGIN_DURATION * 1000); + System.out.println("Note: Accepting clients again!"); + } + } + + private static final int SLEEP_BETWEEN_LOGIN_DURATION = 122; + + private boolean hasQuit = false; + + public synchronized void quit(final String reason) { + if (hasQuit) + return; + + hasQuit = true; + // disconnect from hedgewars server + if (hwcon != null) + hwcon.disconnect(reason); + // disconnect irc client + sendQuit("Quit: " + reason); + // wait some time so that last data can be pushed + try { + Thread.sleep(200); + } + catch (Exception e) { } + // terminate + terminateConnection = true; + } + + + private static String hwActionToIrc(final String chatMsg) { + if (!chatMsg.startsWith("/me ") || (chatMsg.length() <= 4)) + return chatMsg; + + return MAGIC_BYTE_ACTION + "ACTION " + chatMsg.substring(4) + MAGIC_BYTE_ACTION; + } + + private static String ircActionToHw(final String chatMsg) { + if (!chatMsg.startsWith(MAGIC_BYTE_ACTION + "ACTION ") || (chatMsg.length() <= 9)) + return chatMsg; + + return "/me " + chatMsg.substring(8, chatMsg.length() - 1); + } + +// TODO: why is still still being called when joining bogus channel name? + public void join(String channelName) + { + if (ownPlayer == null) { + sendSelfNotice("Trying to join while ownPlayer == null. Aborting!"); + quit("Something went horribly wrong."); + return; + } + + + final Channel channel = getChannel(channelName); + + // TODO reserve special char for creating a new ROOM + // it will be named after the player name by default + // can be changed with /topic after + + // not a valid channel + if (channel == null) { + sendSelfNotice("You cannot manually create channels here."); + sendGlobal(ERR_NOSUCHCHANNEL + ownIrcNick + " " + channel.name + + " :No such channel"); + return; + } + + // TODO if inRoom "Can't join rooms while still in room" + + // TODO set this based on room host/admin mode maybe + +/* :testuser2131!~r@asdasdasdasd.at JOIN #asdkjasda +:weber.freenode.net MODE #asdkjasda +ns +:weber.freenode.net 353 testuser2131 @ #asdkjasda :@testuser2131 +:weber.freenode.net 366 testuser2131 #asdkjasda :End of /NAMES list. +:weber.freenode.net NOTICE #asdkjasda :[freenode-info] why register and identify? your IRC nick is how people know you. http://freenode.net/faq.shtml#nicksetup + +*/ + send(":" + ownPlayer.getIrcId() + " JOIN " + + channelName); + + //send(":sheeppidgin!~r@localhost JOIN " + channelName); + + ircJoined = true; + + sendGlobal(":hw2irc MODE #lobby +nt"); + + sendTopic(channel); + + sendNames(channel); + + } + + private void sendTopic(final Channel channel) { + if (channel.topic != null) + sendGlobal(RPL_TOPIC + ownIrcNick + " " + channel.name + + " :" + channel.topic); + else + sendGlobal(RPL_NOTOPIC + ownIrcNick + " " + channel.name + + " :No topic is set"); + } + + private void sendNames(final Channel channel) { + // There is no error reply for bad channel names. + + if (channel != null) { + // send player list + for (final Player player : channel.players.values()) { + + final String prefix; + + if (player.isServerAdmin()) + prefix = (player.isServerAdmin())?"@":"%"; + else + prefix = (player.isRegistered())?"+":""; + + sendGlobal(RPL_NAMREPLY + ownIrcNick + " = " + channel.name + + " :" + prefix + player.ircNick); + } + } + + sendGlobal(RPL_ENDOFNAMES + ownIrcNick + " " + channel.name + + " :End of /NAMES list"); + } + + private void sendList(final String filter) { + // id column size + //int idl = 1 + String.valueOf(lastRoomId).length(); + + //if (idl < 3) + //idl = 3; + + // send rooms list + sendGlobal(RPL_LISTSTART + ownIrcNick + //+ String.format(" %1$" + idl + "s #P #T Name", "ID")); + + String.format(" %1$s #P #T Name", "ID")); + + if (filter.isEmpty() || filter.equals(".")) { + // lobby + if (filter.isEmpty()) + sendGlobal(RPL_LIST + ownIrcNick + " " + LOBBY_CHANNEL_NAME + + " " + allPlayers.size() + " :" + lobbyChannel.topic); + + // room list could be changed by server while we reply client + synchronized (roomsSorted) { + for (final Room room : roomsSorted) { + sendGlobal(RPL_LIST + ownIrcNick + //+ String.format(" %1$" + idl + "s %2$2d :%3$2d %4$s", + + String.format(" %1$s %2$d :%3$d %4$s", + room.chan, room.nPlayers, room.nTeams, room.name)); + } + } + } + // TODO filter + + sendGlobal(RPL_LISTEND + ownIrcNick + " " + " :End of /LIST"); + } + + private List findPlayers(final String expr) { + List matches = new ArrayList(allPlayers.size()); + + try { + final int flags = Pattern.CASE_INSENSITIVE + Pattern.UNICODE_CASE; + final Pattern regx = Pattern.compile(expr, flags); + + for (final Player p : allPlayers.values()) { + if ((regx.matcher(p.nick).find()) + || (regx.matcher(p.ircId).find()) + //|| (regx.matcher(p.version).find()) + //|| ((p.ip.length() > 2) && regx.matcher(p.ip).find()) + || (!p.getRoom().isEmpty() && regx.matcher(p.getRoom()).find()) + ) matches.add(p); + } + } + catch(PatternSyntaxException ex) { + sendSelfNotice("Pattern not understood: " + ex.getMessage()); + } + + return matches; + } + + private String buildInfoString(final String ip, final String version, final String room) { + return (ip.equals("[]")?"":ip + " ") + version + (room.isEmpty()?"":" " + room); + } + + private void sendWhoForPlayer(final Player player) { + sendWhoForPlayer(LOBBY_CHANNEL_NAME, player.ircNick, (player.inRoom?player.getRoom():""), player.getIrcHostname()); + } + + private void sendWhoForPlayer(final Player player, final String info) { + sendWhoForPlayer(LOBBY_CHANNEL_NAME, player.ircNick, info, player.getIrcHostname()); + } + + private void sendWhoForPlayer(final String nick, final String info) { + sendWhoForPlayer(LOBBY_CHANNEL_NAME, nick, info); + } + + private void sendWhoForPlayer(final String channel, final String nick, final String info) { + final Player player = allPlayers.get(nick); + + if (player == null) + sendWhoForPlayer("OFFLINE", hwToIrcNick(nick), info, "hw/offline"); + else + sendWhoForPlayer(channel, player.ircNick, info, player.getIrcHostname()); + } + + private void sendWhoForPlayer(final String channel, final String ircNick, final String info, final String hostname) { + sendGlobal(RPL_WHOREPLY + channel + " " + channel + + " ~" + ircNick + " " + hostname + + " " + globalServerName + " " + ircNick + + " H :0 " + info); + } + + private void sendWhoEnd(final String ofWho) { + send(RPL_ENDOFWHO + ownIrcNick + " " + ofWho + + " :End of /WHO list."); + } + + private void sendMotd() { + sendGlobal(RPL_MOTDSTART + ownIrcNick + " :- Message of the Day -"); + final String mline = RPL_MOTD + ownIrcNick + " :"; + for(final String line : DEFAULT_MOTD) { + sendGlobal(mline + line); + } + sendGlobal(RPL_ENDOFMOTD + ownIrcNick + " :End of /MOTD command."); + } + + private Channel getChannel(final String name) { + if (name.equals(LOBBY_CHANNEL_NAME)) { + return lobbyChannel; + } + + return null; + } + + private enum Command + { + PASS(1, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.passwordHash = args[0]; + } + }, + NICK(1, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.setNick(args[0]); + } + }, + USER(1, 4) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + if (con.username != null) + { + con.send("NOTICE AUTH :You can't change your user " + + "information after you've logged in right now."); + return; + } + con.username = args[0]; + String forDescription = args.length > 3 ? args[3] + : "(no description)"; + con.description = forDescription; + /* + * Now we'll send the user their initial information. + */ + con.sendGlobal(RPL_WELCOME + con.ownIrcNick + " :Welcome to " + + globalServerName + " - " + DESCRIPTION_SHORT); + con.sendGlobal("004 " + con.ownIrcNick + " " + globalServerName + + " " + VERSION); + + con.sendMotd(); + + } + }, + MOTD(0, 0) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.sendMotd(); + } + }, + PING(1, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.ircPingQueue.add(args[0]); + con.hwcon.sendPing(); + } + }, + PONG(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + con.hwcon.sendPong(); + } + }, + NAMES(1, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final Channel channel = con.getChannel(args[0]); + con.sendNames(channel); + } + }, + LIST(0, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + // TODO filter by args[1] (comma sep list of chans), make # optional + // ignore args[1] (server), TODO: maybe check and send RPL_NOSUCHSERVER if wrong + con.sendList(args.length > 0?args[0]:""); + } + }, + JOIN(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + if (args.length < 1) { + con.sendSelfNotice("You didn't specify what you want to join!"); + return; + } + + if (con.ownPlayer == null) { + con.sendSelfNotice("Lobby is not ready to be joined yet - hold on a second!"); + return; + } + + if (args[0].equals(LOBBY_CHANNEL_NAME)) { + //con.sendSelfNotice("Lobby can't be joined manually!"); + con.join(LOBBY_CHANNEL_NAME); + return; + } + con.sendSelfNotice("Joining rooms is not supported yet, sorry!"); + } + }, + WHO(0, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + if (args.length < 1) + return; + + final String target = args[0]; + + Map players = null; + + if (target.equals(LOBBY_CHANNEL_NAME)) { + players = con.allPlayers; + } + // on channel join WHO is called on channel + else if (target.equals(ROOM_CHANNEL_NAME)) { + players = con.roomPlayers; + } + + if (players != null) { + for (final Player player : players.values()) { + con.sendWhoForPlayer(player); + } + } + // not a known channel. assume arg is player name + // TODO support search expressions! + else { + final String nick = ircToHwNick(target); + final Player player = con.allPlayers.get(nick); + if (player != null) + con.sendWhoForPlayer(player); + else { + con.sendSelfNotice("WHO: No player named " + nick + ", interpreting term as pattern."); + List matches = con.findPlayers(target); + if (matches.isEmpty()) + con.sendSelfNotice("No Match."); + else { + for (final Player match : matches) { + con.sendWhoForPlayer(match); + } + } + } + } + + con.sendWhoEnd(target); + } + }, + WHOIS(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + // there's an optional param in the beginning that we don't care about + final String targets = args[args.length-1]; + for (final String target : targets.split(",")) { + if (target.isEmpty()) + continue; + final String nick = ircToHwNick(target); + // flag this nick as manually requested, so that response is shown + if (con.ircJoined) { + con.requestedInfos.add(nick); + con.hwcon.requestInfo(nick); + } + + final Player player = con.allPlayers.get(nick); + if (player != null) { + con.send(RPL_WHOISUSER + con.ownIrcNick + " " + target + " ~" + + target + " " + player.getIrcHostname() + " * : " + + player.ircNick); + // TODO send e.g. channels: @#lobby or @#123 + con.send(RPL_ENDOFWHOIS + con.ownIrcNick + " " + target + + " :End of /WHOIS list."); + } + } + } + }, + USERHOST(1, 5) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + /* + // TODO set server host + con.hostname = "hw/" + SERVER_HOST; + + ArrayList replies = new ArrayList(); + for (String s : arguments) + { + Connection user = connectionMap.get(s); + if (user != null) + replies.add(user.nick + "=+" + con.ownIrc + "@" + + con.hostname); + } + con.sendGlobal(RPL_USERHOST + con.ownIrcNick + " :" + + delimited(replies.toArray(new String[0]), " ")); + */ + } + }, + MODE(0, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final boolean forChan = args[0].startsWith("#"); + + if (args.length == 1) + { + if (forChan) { + //con.sendGlobal(ERR_NOCHANMODES + args[0] + // + " :Channel doesn't support modes"); + con.sendGlobal(RPL_CHANNELMODEIS + con.ownIrcNick + " " + args[0] + + " +nt"); + } + else + { + // TODO + con.sendSelfNotice("User mode querying not supported yet."); + } + } + else if (args.length == 2) { + + if (forChan) { + + final int l = args[1].length(); + + for (int i = 0; i < l; i++) { + + final char c = args[1].charAt(i); + + switch (c) { + case '+': + // skip + break; + case '-': + // skip + break; + case 'b': + con.sendGlobal(RPL_ENDOFBANLIST + + con.ownIrcNick + " " + args[0] + + " :End of channel ban list"); + break; + case 'e': + con.sendGlobal(RPL_ENDOFEXCEPTLIST + + con.ownIrcNick + " " + args[0] + + " :End of channel exception list"); + break; + default: + con.sendGlobal(ERR_UNKNOWNMODE + c + + " :Unknown MODE flag " + c); + break; + + } + } + } + // user mode + else { + con.sendGlobal(ERR_UMODEUNKNOWNFLAG + args[0] + + " :Unknown MODE flag"); + } + } + else + { + con.sendSelfNotice("Specific modes not supported yet."); + } + } + }, + PART(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + String[] channels = args[0].split(","); + boolean doQuit = false; + + for (String channelName : channels) + { + if (channelName.equals(LOBBY_CHANNEL_NAME)) { + doQuit = true; + } + // TODO: part from room + /* + synchronized (mutex) + { + Channel channel = channelMap.get(channelName); + if (channelName == null) + con + .sendSelfNotice("You're not a member of the channel " + + channelName + + ", so you can't part it."); + else + { + channel.send(":" + con.getRepresentation() + + " PART " + channelName); + channel.channelMembers.remove(con); + if (channel.channelMembers.size() == 0) + channelMap.remove(channelName); + } + } + */ + } + + final String reason; + + if (args.length > 1) + reason = args[1]; + else + reason = DEFAULT_QUIT_REASON; + + // quit after parting + if (doQuit) + con.quit(reason); + } + }, + QUIT(0, 1) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final String reason; + + if (args.length == 0) + reason = DEFAULT_QUIT_REASON; + else + reason = args[0]; + + con.quit(reason); + } + }, + PRIVMSG(2, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + String message = ircActionToHw(args[1]); + if (message.charAt(0) == CHAT_COMMAND_CHAR) { + if (message.length() < 1 ) + return; + message = message.substring(1); + // TODO maybe \rebind CUSTOMCMDCHAR command + con.hwcon.sendCommand(con.ircToHwNick(message)); + } + else + con.hwcon.sendChat(con.ircToHwNick(message)); + } + }, + TOPIC(1, 2) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final String chan = args[0]; + + final Channel channel = con.getChannel(chan); + + if (channel == null) { + con.sendSelfNotice("No such channel for topic viewing: " + + chan); + return; + } + + // The user wants to see the channel topic. + if (args.length == 1) + con.sendTopic(channel); + // The user wants to set the channel topic. + else + channel.topic = args[1]; + } + }, + KICK(3, 3) + { + @Override + public void run(final Connection con, final String prefix, final String[] args) + throws Exception + { + final String victim = args[1]; + con.logInfo("Issuing kick for " + victim); + // "KICK #channel nick :kick reason (not relevant)" + con.hwcon.kick(ircToHwNick(victim)); + } + } + ; + public final int minArgumentCount; + public final int maxArgumentCount; + + private Command(int min, int max) + { + minArgumentCount = min; + maxArgumentCount = max; + } + + public abstract void run(Connection con, String prefix, + String[] arguments) throws Exception; + } + + public static String delimited(String[] items, String delimiter) + { + StringBuffer response = new StringBuffer(); + boolean first = true; + for (String s : items) + { + if (first) + first = false; + else + response.append(delimiter); + response.append(s); + } + return response.toString(); + } + + protected void sendQuit(String quitMessage) + { + send(":" + getRepresentation() + " QUIT :" + quitMessage); + } + + @Override + public void run() + { + try + { + doServer(); + } + catch (Exception e) { + e.printStackTrace(); + } + finally + { + // TODO sense? + if (ownIrcNick != null && connectionMap.get(ownIrcNick) == this) { + quit("Client disconnected."); + } + + try { + socket.close(); + } + catch (Exception e) { } + + quit("Connection terminated."); + } + } + + protected void sendGlobal(String string) + { + send(":" + globalServerName + " " + string); + } + + private LinkedBlockingQueue outQueue = new LinkedBlockingQueue( + 1000); + + private Thread outThread = new Thread() + { + public void run() + { + try + { + OutputStream out = socket.getOutputStream(); + while (!terminateConnection) + { + String s = outQueue.take(); + s = s.replace("\n", "").replace("\r", ""); + s = s + "\r\n"; + out.write(s.getBytes()); + out.flush(); + } + } + catch (Exception ex) + { + thisConnection.logError("Outqueue died"); + //ex.printStackTrace(); + } + finally { + outQueue.clear(); + outQueue = null; + try + { + socket.close(); + } + catch (Exception e2) + { + e2.printStackTrace(); + } + } + } + }; + + private boolean terminateConnection = false; + + private void doServer() throws Exception + { + outThread.start(); + InputStream socketIn = socket.getInputStream(); + BufferedReader clientReader = new BufferedReader(new InputStreamReader( + socketIn)); + String line; + while (!terminateConnection && ((line = clientReader.readLine()) != null)) + { + processLine(line); + } + } + + public void sanitizeInputs(final String[] inputs) { + + // no for-each loop, because we need write access to the elements + + final int l = inputs.length; + + for (int i = 0; i < l; i++) { + inputs[i] = inputs[i].replaceAll(MAGIC_BYTES, " "); + } + } + + private void processLine(final String line) throws Exception + { + String l = line; + + // log things + if (l.startsWith("PASS") || l.startsWith("pass")) + this.logInfo("IRC-Client provided PASS"); + else + this.logDebug("IRC-Client: " + l); + + String prefix = ""; + if (l.startsWith(":")) + { + String[] tokens = l.split(" ", 2); + prefix = tokens[0]; + l = (tokens.length > 1 ? tokens[1] : ""); + } + String[] tokens1 = l.split(" ", 2); + String command = tokens1[0]; + l = tokens1.length > 1 ? tokens1[1] : ""; + String[] tokens2 = l.split("(^| )\\:", 2); + String trailing = null; + l = tokens2[0]; + if (tokens2.length > 1) + trailing = tokens2[1]; + ArrayList argumentList = new ArrayList(); + if (!l.equals("")) + argumentList.addAll(Arrays.asList(l.split(" "))); + if (trailing != null) + argumentList.add(trailing); + final String[] args = argumentList.toArray(new String[0]); + + // process command + + // numeric commands + if (command.matches("[0-9][0-9][0-9]")) + command = "N" + command; + + final Command commandObject; + + try { + commandObject = Command.valueOf(command.toUpperCase()); + } + catch (Exception ex) { + // forward raw unknown command to hw server + hwcon.sendCommand(ircToHwNick(line)); + return; + } + + if (args.length < commandObject.minArgumentCount + || args.length > commandObject.maxArgumentCount) + { + sendSelfNotice("Invalid number of arguments for this" + + " command, expected not more than " + + commandObject.maxArgumentCount + " and not less than " + + commandObject.minArgumentCount + " but got " + args.length + + " arguments"); + return; + } + commandObject.run(this, prefix, args); + } + + /** + * Sends a notice from the server to the user represented by this + * connection. + * + * @param string + * The text to send as a notice + */ + + private void sendSelfNotice(final String string) + { + send(":" + globalServerName + " NOTICE " + ownIrcNick + " :" + string); + } + + private void sendChannelNotice(final String string) { + sendChannelNotice(string, globalServerName); + } + + private void sendChannelNotice(final String string, final String from) { + // TODO send to room if user is in room + send(":" + from + " NOTICE " + LOBBY_CHANNEL_NAME + " :" + string); + } + + private void sendServerNotice(final String string) + { + if (ircJoined) + sendChannelNotice(string, "[INFO]"); + + sendSelfNotice(string); + } + + private String[] padSplit(final String line, final String regex, int max) + { + String[] split = line.split(regex); + String[] output = new String[max]; + for (int i = 0; i < output.length; i++) + { + output[i] = ""; + } + for (int i = 0; i < split.length; i++) + { + output[i] = split[i]; + } + return output; + } + + public void sendIfJoined(final String s) { + if (joined) + send(s); + } + + public void send(final String s) + { + final Queue testQueue = outQueue; + if (testQueue != null) + { + this.logDebug("IRC-Server: " + s); + testQueue.add(s); + } + } + +final static String RPL_WELCOME = "001 "; +final static String RPL_YOURHOST = "002 "; +final static String RPL_CREATED = "003 "; +final static String RPL_MYINFO = "004 "; +final static String RPL_BOUNCE = "005 "; +final static String RPL_TRACELINK = "200 "; +final static String RPL_TRACECONNECTING = "201 "; +final static String RPL_TRACEHANDSHAKE = "202 "; +final static String RPL_TRACEUNKNOWN = "203 "; +final static String RPL_TRACEOPERATOR = "204 "; +final static String RPL_TRACEUSER = "205 "; +final static String RPL_TRACESERVER = "206 "; +final static String RPL_TRACESERVICE = "207 "; +final static String RPL_TRACENEWTYPE = "208 "; +final static String RPL_TRACECLASS = "209 "; +final static String RPL_TRACERECONNECT = "210 "; +final static String RPL_STATSLINKINFO = "211 "; +final static String RPL_STATSCOMMANDS = "212 "; +final static String RPL_STATSCLINE = "213 "; +final static String RPL_STATSNLINE = "214 "; +final static String RPL_STATSILINE = "215 "; +final static String RPL_STATSKLINE = "216 "; +final static String RPL_STATSQLINE = "217 "; +final static String RPL_STATSYLINE = "218 "; +final static String RPL_ENDOFSTATS = "219 "; +final static String RPL_UMODEIS = "221 "; +final static String RPL_SERVICEINFO = "231 "; +final static String RPL_ENDOFSERVICES = "232 "; +final static String RPL_SERVICE = "233 "; +final static String RPL_SERVLIST = "234 "; +final static String RPL_SERVLISTEND = "235 "; +final static String RPL_STATSVLINE = "240 "; +final static String RPL_STATSLLINE = "241 "; +final static String RPL_STATSUPTIME = "242 "; +final static String RPL_STATSOLINE = "243 "; +final static String RPL_STATSHLINE = "244 "; +final static String RPL_STATSPING = "246 "; +final static String RPL_STATSBLINE = "247 "; +final static String RPL_STATSDLINE = "250 "; +final static String RPL_LUSERCLIENT = "251 "; +final static String RPL_LUSEROP = "252 "; +final static String RPL_LUSERUNKNOWN = "253 "; +final static String RPL_LUSERCHANNELS = "254 "; +final static String RPL_LUSERME = "255 "; +final static String RPL_ADMINME = "256 "; +final static String RPL_ADMINEMAIL = "259 "; +final static String RPL_TRACELOG = "261 "; +final static String RPL_TRACEEND = "262 "; +final static String RPL_TRYAGAIN = "263 "; +final static String RPL_NONE = "300 "; +final static String RPL_AWAY = "301 "; +final static String RPL_USERHOST = "302 "; +final static String RPL_ISON = "303 "; +final static String RPL_UNAWAY = "305 "; +final static String RPL_NOWAWAY = "306 "; +final static String RPL_WHOISUSER = "311 "; +final static String RPL_WHOISSERVER = "312 "; +final static String RPL_WHOISOPERATOR = "313 "; +final static String RPL_WHOWASUSER = "314 "; +final static String RPL_ENDOFWHO = "315 "; +final static String RPL_WHOISCHANOP = "316 "; +final static String RPL_WHOISIDLE = "317 "; +final static String RPL_ENDOFWHOIS = "318 "; +final static String RPL_WHOISCHANNELS = "319 "; +final static String RPL_LISTSTART = "321 "; +final static String RPL_LIST = "322 "; +final static String RPL_LISTEND = "323 "; +final static String RPL_CHANNELMODEIS = "324 "; +final static String RPL_UNIQOPIS = "325 "; +final static String RPL_NOTOPIC = "331 "; +final static String RPL_TOPIC = "332 "; +final static String RPL_INVITING = "341 "; +final static String RPL_SUMMONING = "342 "; +final static String RPL_INVITELIST = "346 "; +final static String RPL_ENDOFINVITELIST = "347 "; +final static String RPL_EXCEPTLIST = "348 "; +final static String RPL_ENDOFEXCEPTLIST = "349 "; +final static String RPL_VERSION = "351 "; +final static String RPL_WHOREPLY = "352 "; +final static String RPL_NAMREPLY = "353 "; +final static String RPL_KILLDONE = "361 "; +final static String RPL_CLOSING = "362 "; +final static String RPL_CLOSEEND = "363 "; +final static String RPL_LINKS = "364 "; +final static String RPL_ENDOFLINKS = "365 "; +final static String RPL_ENDOFNAMES = "366 "; +final static String RPL_BANLIST = "367 "; +final static String RPL_ENDOFBANLIST = "368 "; +final static String RPL_ENDOFWHOWAS = "369 "; +final static String RPL_INFO = "371 "; +final static String RPL_MOTD = "372 "; +final static String RPL_INFOSTART = "373 "; +final static String RPL_ENDOFINFO = "374 "; +final static String RPL_MOTDSTART = "375 "; +final static String RPL_ENDOFMOTD = "376 "; +final static String RPL_YOUREOPER = "381 "; +final static String RPL_REHASHING = "382 "; +final static String RPL_YOURESERVICE = "383 "; +final static String RPL_MYPORTIS = "384 "; +final static String RPL_TIME = "391 "; +final static String RPL_USERSSTART = "392 "; +final static String RPL_USERS = "393 "; +final static String RPL_ENDOFUSERS = "394 "; +final static String RPL_NOUSERS = "395 "; +final static String ERR_NOSUCHNICK = "401 "; +final static String ERR_NOSUCHSERVER = "402 "; +final static String ERR_NOSUCHCHANNEL = "403 "; +final static String ERR_CANNOTSENDTOCHAN = "404 "; +final static String ERR_TOOMANYCHANNELS = "405 "; +final static String ERR_WASNOSUCHNICK = "406 "; +final static String ERR_TOOMANYTARGETS = "407 "; +final static String ERR_NOSUCHSERVICE = "408 "; +final static String ERR_NOORIGIN = "409 "; +final static String ERR_NORECIPIENT = "411 "; +final static String ERR_NOTEXTTOSEND = "412 "; +final static String ERR_NOTOPLEVEL = "413 "; +final static String ERR_WILDTOPLEVEL = "414 "; +final static String ERR_BADMASK = "415 "; +final static String ERR_UNKNOWNCOMMAND = "421 "; +final static String ERR_NOMOTD = "422 "; +final static String ERR_NOADMININFO = "423 "; +final static String ERR_FILEERROR = "424 "; +final static String ERR_NONICKNAMEGIVEN = "431 "; +final static String ERR_ERRONEUSNICKNAME = "432 "; +final static String ERR_NICKNAMEINUSE = "433 "; +final static String ERR_NICKCOLLISION = "436 "; +final static String ERR_UNAVAILRESOURCE = "437 "; +final static String ERR_USERNOTINCHANNEL = "441 "; +final static String ERR_NOTONCHANNEL = "442 "; +final static String ERR_USERONCHANNEL = "443 "; +final static String ERR_NOLOGIN = "444 "; +final static String ERR_SUMMONDISABLED = "445 "; +final static String ERR_USERSDISABLED = "446 "; +final static String ERR_NOTREGISTERED = "451 "; +final static String ERR_NEEDMOREPARAMS = "461 "; +final static String ERR_ALREADYREGISTRED = "462 "; +final static String ERR_NOPERMFORHOST = "463 "; +final static String ERR_PASSWDMISMATCH = "464 "; +final static String ERR_YOUREBANNEDCREEP = "465 "; +final static String ERR_YOUWILLBEBANNED = "466 "; +final static String ERR_KEYSET = "467 "; +final static String ERR_CHANNELISFULL = "471 "; +final static String ERR_UNKNOWNMODE = "472 "; +final static String ERR_INVITEONLYCHAN = "473 "; +final static String ERR_BANNEDFROMCHAN = "474 "; +final static String ERR_BADCHANNELKEY = "475 "; +final static String ERR_BADCHANMASK = "476 "; +final static String ERR_NOCHANMODES = "477 "; +final static String ERR_BANLISTFULL = "478 "; +final static String ERR_NOPRIVILEGES = "481 "; +final static String ERR_CHANOPRIVSNEEDED = "482 "; +final static String ERR_CANTKILLSERVER = "483 "; +final static String ERR_RESTRICTED = "484 "; +final static String ERR_UNIQOPPRIVSNEEDED = "485 "; +final static String ERR_NOOPERHOST = "491 "; +final static String ERR_NOSERVICEHOST = "492 "; +final static String ERR_UMODEUNKNOWNFLAG = "501 "; +final static String ERR_USERSDONTMATCH = "502 "; + +} diff -r 6409d756e9da -r 823cf18be1fc tools/hw2irc/restart --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tools/hw2irc/restart Wed May 26 16:32:43 2021 -0400 @@ -0,0 +1,8 @@ +#!/bin/bash +sudo -u nobody killall java; sleep 3; sudo -u nobody killall -9 java; +sleep 30; +#sudo -u nobody java net.ercatec.hw2ircsvr.Connection netserver.hedgewars.org 1337 >>out.sheepy 2>>err.sheepy & disown +sudo -u nobody bash -c 'java net.ercatec.hw2ircsvr.Connection netserver.hedgewars.org 1337 >>out.sheepy 2>&1 & disown' +sleep 122 +#sudo -u nobody java net.ercatec.hw2ircsvr.Connection >>out 2>>err & disown +sudo -u nobody bash -c 'java net.ercatec.hw2ircsvr.Connection >>out 2>&1 & disown'