--- /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 <sheepluva@ercatec.net>
+ *
+ * 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<String> parts = new ArrayList<String>(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");
+ }
+}
+
--- /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 <sheepluva@ercatec.net>
+ *
+ * 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<String> parts;
+
+ // invalid Message
+ public ProtocolMessage() {
+ this.parts = Arrays.asList(emptyArgs);
+ this.args = emptyArgs;
+ this.isValid = false;
+ }
+
+ public ProtocolMessage(final List<String> 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<String> parts)
+ {
+ final Iterator<String> 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);
+ }
+}
--- /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<String, Player> allPlayers = new Hashtable<String, Player>();
+ private final Map<String, Player> roomPlayers = new Hashtable<String, Player>();
+ private final Map<Integer, Room> roomsById = new Hashtable<Integer, Room>();
+ private final Map<String, Room> roomsByName = new Hashtable<String, Room>();
+ private final List<Room> roomsSorted = new Vector<Room>();
+
+ private final List<String> ircPingQueue = new Vector<String>();
+
+ 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<Player> newPlayers = new ArrayList<Player>(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<String> requestedInfos = new Vector<String>();
+
+ 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<String, Player> players;
+
+ public Channel(final String name, final String topic, final Map<String, Player> 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<String, Connection> connectionMap = new HashMap<String, Connection>();
+ // TODO those MUST NOT be static!
+ //private Map<String, Channel> channelMap = new HashMap<String, Channel>();
+ 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<Player> findPlayers(final String expr) {
+ List<Player> matches = new ArrayList<Player>(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<String, Player> 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<Player> 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<String> replies = new ArrayList<String>();
+ 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<String> outQueue = new LinkedBlockingQueue<String>(
+ 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<String> argumentList = new ArrayList<String>();
+ 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<String> 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 ";
+
+}