/*
* (c) Copyright 2007-2008 by Edgar Kalkowski (eMail@edgar-kalkowski.de)
*
* The ErkiTalk Chat Server 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; either version 3 of the License, or (at your
* option) any later version.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
package erki.talk.server;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import erki.api.util.Logger;
import erki.talk.Crypto;
import erki.talk.DHKeyExchange;
/**
* This class is a tcp talk server.
*
* @author Edgar Kalkowski
*/
public class ErkiTalkServer implements Runnable {
/** All the log messages go to this file. */
private static String logFile = "ErkiTalkServer.log";
/** The version string of the server. */
private static final String VERSION = "v3.1.0";
/** The file containing the md5's of all registered user's passwords. */
private static final String PWD_FILE = "passwd";
/** The timestamp when this server was started. */
private final long startTimestamp;
/** The timestamp when the topic was set. */
private long topicTimestamp = System.currentTimeMillis();
/** The user who set the current topic. */
private String topicUser = "ErkiTalkServer";
/** The port the server is running on. */
private int port;
/** The current topic of this chat. */
private String topic = "cd /pub; more beer";
/** The list of all clients. */
private List<Client> clients = new LinkedList<Client>();
/**
* Contains the hex strings of the sha-512's of the passwords of all
* registered users.
*/
private TreeMap<String, String> pwds;
/** This static block initializes the log file. */
static {
try {
Logger.setHander(new PrintStream(
new FileOutputStream(logFile, true), true, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
System.exit(-1);
} catch (FileNotFoundException e) {
e.printStackTrace();
System.exit(-1);
}
Logger.print(ErkiTalkServer.class, "This is Erki der Loony's talk "
+ "server " + VERSION + ". Please have fun!");
}
/**
* Initializes a new instance of this talk server listening on a specific
* port.
*
* @param port
* The port the server shall be running on.
*/
public ErkiTalkServer(int port) {
this.port = port;
this.startTimestamp = System.currentTimeMillis();
loadPwds();
// Start a ping thread
new Thread() {
@Override
public void run() {
super.run();
while (true) {
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
if (c.ping == false) {
c.send("PING");
c.ping = true;
} else {
c.kill("PING timeout");
}
}
try {
Thread.sleep(300000);
} catch (InterruptedException e) {
}
}
}
}.start();
}
@SuppressWarnings("unchecked")
private void loadPwds() {
try {
ObjectInputStream objectIn = new ObjectInputStream(
new FileInputStream(PWD_FILE));
pwds = (TreeMap<String, String>) objectIn.readObject();
objectIn.close();
} catch (FileNotFoundException e) {
// Do nothing. A pwd-file will be generated if needed.
pwds = new TreeMap<String, String>();
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
} catch (ClassNotFoundException e) {
e.printStackTrace();
System.exit(-1);
}
}
private void savePwds() {
try {
ObjectOutputStream objectOut = new ObjectOutputStream(
new FileOutputStream(PWD_FILE));
objectOut.writeObject(pwds);
objectOut.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
System.exit(-1);
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
/**
* Main server method that listens for clients and creates a new thread for
* each one.
*/
@Override
public void run() {
ServerSocket socket = null;
Socket clientSocket;
try {
socket = new ServerSocket(port);
} catch (IOException e) {
Logger.error(this, "Could not aquire socket!");
System.exit(-1);
}
while (true) {
try {
clientSocket = socket.accept();
} catch (IOException e) {
Logger.error(this, "Error while waiting for clients! Server "
+ "will restart in 5 min.");
try {
socket.close();
} catch (IOException e1) {
}
try {
Thread.sleep(300000);
} catch (InterruptedException ex) {
}
break;
}
Client client = null;
try {
client = new Client(clientSocket);
} catch (IOException e) {
Logger.error(this, "Could not create new client instance!");
try {
clientSocket.close();
} catch (IOException e1) {
}
}
Logger.info(this, "A new client has connected from "
+ clientSocket.getInetAddress().getCanonicalHostName()
+ ".");
client.start();
}
}
/** A specified client sends text to all connected clients. */
public void sendText(Client client, String line) {
String msg = "TEXT " + client.getNick() + ": " + line;
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send(msg);
}
}
/** Send an information to a specific client. */
public void sendInfo(String line, Client client) {
client.send("INFO " + line);
}
/** Notify all clients that the topic has changed. */
public void sendTopicChange(Client client, String newTopic) {
String msg = "NEWTOPIC " + client.getNick() + ": " + newTopic;
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send(msg);
}
}
/**
* Notify all clients that someone has changed the nick (and remind everyone
* that the person with the new nick is not authed).
*/
public void sendNickChange(String oldNick, String newNick) {
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send("ISAUTHED " + oldNick + ": FALSE");
c.send("NEWNICK " + oldNick + ": " + newNick);
}
}
/** Notify all clients that someone joined the chat. */
public void sendJoin(Client client) {
String msg = "JOIN " + client.getNick();
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send(msg);
}
}
/** Notify all clients that someone has left the chat. */
public void sendQuit(Client client, String reason) {
String msg;
Client[] cArray;
if (reason != null) {
msg = "QUIT " + client.getNick() + ": " + reason;
} else {
msg = "QUIT " + client.getNick();
}
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send(msg);
}
}
/** Notify a client that he has successfully registered an account. */
public void sendRegistered(Client client) {
client.send("REGISTERED");
}
/** Notify a client that his password was successfully deleted. */
void sendDeregistered(Client client) {
client.send("DEREGISTERED");
}
/**
* Notify a client that all further communication will be encrypted. The
* number transmitted allows the client to calculate the secret key. This is
* the last message to be transmitted unencrypted. All other uses are
* informed that this user switched to an encrypted connection.
*/
public void sendEncrypt(Client client, String pubkey) {
client.send("ENCRYPT " + pubkey);
}
/**
* Notify all the clients that someone switched to an encrypted connection.
* The client that is now encrypted also receives this notice (but of course
* already encrypted).
*/
public void sendEncryptInfo(Client client) {
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send("ISENCRYPTED " + client.getNick() + ": TRUE");
}
}
/**
* Notify a client that his authentification was successful and all others
* that the client has authed.
*/
public void sendAuth(Client client) {
client.send("AUTHED");
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send("ISAUTHED " + client.getNick() + ": TRUE");
}
}
/** Notify a client if another client uses an encrypted connection. */
public void sendIsEncrypted(Client client, Client encClient) {
if (encClient.encrypt) {
client.send("ISENCRYPTED " + encClient.getNick() + ": TRUE");
} else {
client.send("ISENCRYPTED " + encClient.getNick() + ": FALSE");
}
}
/** Notify a client if another client is authed at the server. */
public void sendIsAuthed(Client client, Client authedClient) {
if (authedClient.authed) {
client.send("ISAUTHED " + authedClient.getNick() + ": TRUE");
} else {
client.send("ISAUTHED " + authedClient.getNick() + ": FALSE");
}
}
/**
* Notify a client that all further communication will be sent in plain text
* and not encrypted. This message itself is the last one transmitted
* encrypted. Notify all other clients that this client switched back to
* plain text and no longer uses an encrypted connection.
*/
public void sendEndEncrypt(Client client) {
client.send("PLAIN");
}
/**
* Notify all clients that someone canceled encryption and sends plain text
* again. The client that is now sending plain text again also receives this
* notice (of course in plain text).
*/
public void sendEndEncryptInfo(Client client) {
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send("ISENCRYPTED " + client.getNick() + ": FALSE");
}
}
/** Send an error message to a specific client. */
public void sendError(String line, Client client) {
client.send("ERROR " + line);
}
/** Notify a client that the chosen nick is already in use. */
public void sendNickInUse(Client client, String nick) {
client.send("NICKINUSE " + nick);
}
/** A specific client sends text to another client. */
public void sendPM(Client from, String line, Client to) {
to.send("PM " + from.getNick() + ": " + line);
}
/** A specific client sends indirect speech to all other clients. */
public void sendIndirect(Client client, String line) {
String msg = "INDIRECT " + client.getNick() + ": " + line;
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
c.send(msg);
}
}
/** Send the user list to a specific client. */
public void sendList(Client client) {
client.send("BEGINUSERS");
Client[] cArray;
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
client.send("USER " + (c.authed ? "T" : "F")
+ (c.encrypt ? "T" : "F") + " " + c.getNick());
}
client.send("ENDUSERS");
}
/** Responds to a client's ping request. */
public void sendPong(Client client) {
client.send("PONG");
}
/** Sends the time for which the server is running to a client. */
public void sendUptime(Client client) {
client.send("UPTIME " + getTime(startTimestamp));
}
/** Send the chat topic to a specific client. */
public void sendTopic(Client client) {
synchronized (topic) {
client.send("TOPIC " + topicUser + ": " + getTime(topicTimestamp)
+ ": " + topic);
}
}
/** Removes a client e.g. when it has disconnected. */
public void remove(Client client, String reason) {
synchronized (clients) {
if (clients.contains(client)) {
clients.remove(client);
sendQuit(client, reason);
} else {
return;
}
}
}
/** Indicates wether or not a nick is currently in use. */
public boolean isInUse(String nick) {
synchronized (clients) {
for (Client c : clients) {
if (c.getNick() != null && c.getNick().equals(nick)) {
return true;
}
}
}
return false;
}
/** @return The current topic of the chat. */
public String getTopic() {
synchronized (topic) {
return topic;
}
}
/** Changes the topic to a new one. */
public void setTopic(String topic) {
synchronized (topic) {
this.topic = topic;
}
}
/**
* @return A nice string representation of a given unix timestamp. The
* format is "dd.mm.yyyy hh:mm".
*/
public static String getTime(long timestamp) {
String result = "";
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(timestamp);
int day = calendar.get(Calendar.DAY_OF_MONTH);
result += day < 10 ? "0" + day + "." : day + ".";
int month = calendar.get(Calendar.MONTH) + 1;
result += month < 10 ? "0" + month + "." : month + ".";
int year = calendar.get(Calendar.YEAR);
result += year + " ";
int hours = calendar.get(Calendar.HOUR_OF_DAY);
result += hours < 10 ? "0" + hours + ":" : hours + ":";
int mins = calendar.get(Calendar.MINUTE);
result += mins < 10 ? "0" + mins : mins;
return result;
}
/**
* @return A nice string representation of a time difference. The format is
* "[d ]hh:mm:ss".
*/
public static String getTimeDiff(long startTimestamp, long endTimestamp) {
String result = "";
long diff = endTimestamp - startTimestamp;
long sec = diff / 1000;
long min = sec / 60;
sec %= 60;
long hours = min / 60;
min = min %= 60;
long days = hours / 24;
hours %= 24;
result += days + "d ";
result += hours < 10 ? "0" + hours + ":" : hours + ":";
result += min < 10 ? "0" + min + ":" : min + ":";
result += sec < 10 ? "0" + sec : sec;
return result;
}
/**
* Represents a client and stores the client's socket.
*
* @author Edgar Kalkowski
*/
class Client extends Thread {
/** This client's socket. */
private Socket socket;
/** The nickname of this client. */
private String name = null;
/** The output stream of this client's socket. */
private PrintWriter socketOut;
/** The input stream of this client's socket. */
private BufferedReader socketIn;
/** Indicates whether this client's thread shall exit itself. */
private boolean killed = false;
/** Indicates whether this client must pong until the next ping cycle. */
private boolean ping = false;
/**
* Indicates whether this client uses an encrypted connection. If
* {@code true} all text received from this client will be passed
* through {@link Crypto#decrypt(String)} before interpreting and all
* text send to this client will be passed through
* {@link Crypto#encrypt(String)} before send to the client.
*/
private boolean encrypt = false;
/**
* The {@link Crypto} object used by this client to encrypt messages if
* {@link encrypt} ist {@code true}.
*/
private Crypto crypto = null;
/**
* The {@link DHKeyExchange} object used for this client to generate
* public and private keys.
*/
private DHKeyExchange keyExchange = null;
/**
* Indicates whether this client has authed itself with a password
* submitted via an encrypted channel.
*/
private boolean authed = false;
/**
* Creates a new talk server client.
*
* @param socket
* The client socket.
* @throws IOException
*/
public Client(Socket socket) throws IOException {
this.socket = socket;
name = socket.getInetAddress().getCanonicalHostName();
socketOut = new PrintWriter(new OutputStreamWriter(socket
.getOutputStream(), "UTF-8"), true);
socketIn = new BufferedReader(new InputStreamReader(socket
.getInputStream(), "UTF-8"));
}
/**
* Send a line of text to this client.
*
* @param line
* The line of text to send. This {@code String} must not be
* terminated by a "\r" or "\n".
*/
public synchronized void send(String line) {
if (encrypt) {
try {
line = crypto.encrypt(line);
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
System.exit(-1);
} catch (BadPaddingException e) {
e.printStackTrace();
System.exit(-1);
}
}
socketOut.println(line);
}
/** Gives access to this client's nick. */
public String getNick() {
return name;
}
/**
* Set the ping flag that indicated if this client must pong until the
* next ping cycle.
*/
public void setPing(boolean ping) {
this.ping = ping;
}
/** Quits this client for a reason. */
public void quit(String reason) {
sendInfo("Thanks for the visit!", this);
sendInfo("Bye!", this);
kill(reason);
}
/** Kill this client if e.g. it's socket is dead. */
public void kill(String reason) {
killed = true;
if (reason == null) {
Logger.info(this, getNick() + " has left.");
} else {
Logger.info(this, getNick() + " has left (" + reason + ").");
}
remove(this, reason);
try {
socket.close();
} catch (IOException e) {
}
}
/** Handles all messages from this client. */
@Override
public void run() {
try {
String line;
send("PING");
if ((line = socketIn.readLine()) != null) {
if (!line.toUpperCase().equals("PONG")) {
send("I don't like you! Go away!");
Logger.info(this, "The new client does not speak my "
+ "language, so I'll kick it.");
kill("Does not speak ErkiTalk properly");
return;
}
} else {
kill("Lost connection");
return;
}
while (true) {
if ((line = socketIn.readLine()) != null) {
if (line.toUpperCase().startsWith("NICK ")) {
line = line.substring("NICK ".length());
if (line.equals("")) {
sendError("The empty nick is not allowed!",
this);
continue;
} else if (line.contains(":")) {
sendError("Your nick must not contain colons "
+ "(»:«)!", this);
continue;
} else if (isInUse(line)) {
sendNickInUse(this, line);
continue;
} else {
name = line;
Logger.info(this, "The new Client chose the "
+ "nickname »" + name + "«.");
sendJoin(this);
synchronized (clients) {
clients.add(this);
}
break;
}
} else {
sendError("Please submit your nickname!", this);
continue;
}
} else {
kill("Lost connection");
return;
}
}
sendInfo("This is Erki der Loony's talk server " + VERSION
+ ". Please have fun!", this);
sendInfo("The server is running since "
+ getTime(startTimestamp)
+ " for "
+ getTimeDiff(startTimestamp, System
.currentTimeMillis()) + ".", this);
sendInfo("The current chat topic is: " + getTopic(), this);
sendInfo("The topic was set on " + getTime(topicTimestamp)
+ " by " + topicUser + ".", this);
String users = "";
synchronized (clients) {
for (Client c : clients) {
users += c.getNick() + ", ";
}
}
if (users.equals("")) {
users = "keine, ";
}
sendInfo("The following users are currently online: "
+ users.substring(0, users.length() - 2) + ".", this);
while (!killed) {
if ((line = socketIn.readLine()) != null) {
if (encrypt) {
try {
line = crypto.decrypt(line);
} catch (NumberFormatException e) {
sendError("Your encryption is flawed!", this);
sendInfo("I received " + line
+ " which is not a "
+ "correctly encrypted line!", this);
} catch (IllegalBlockSizeException e) {
sendError("Your encryption is flawed!", this);
sendInfo("I received " + line
+ " which is not a "
+ "correctly encrypted line!", this);
} catch (BadPaddingException e) {
sendError("Your encryption is flawed!", this);
sendInfo("I received " + line
+ " which is not a "
+ "correctly encrypted line!", this);
}
}
if (line.toUpperCase().startsWith("AUTH ")) {
String pwd = line.substring("AUTH ".length());
if (!encrypt) {
Logger.info(this, getNick() + " failed to "
+ "auth (No encrypted connection).");
sendError("For your own safety you have to "
+ "transmit your password via an "
+ "encrypted connection!", this);
sendInfo("You can initialize an encrypted "
+ "connection via the »ENCRYPT« "
+ "command.", this);
continue;
}
if (!pwds.containsKey(getNick())) {
Logger.info(this, getNick() + " failed to "
+ "auth (Not registered).");
sendError("Please register at first via "
+ "»REGISTER«!", this);
continue;
}
try {
if (!pwds.get(getNick()).equals(
Crypto.toSHA(pwd))) {
Logger.info(this, getNick() + " failed "
+ "to auth (Wrong password).");
sendError("Wrong password!", this);
continue;
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
System.exit(-1);
}
Logger.info(this, getNick()
+ " successfully authed.");
authed = true;
sendAuth(this);
} else if (line.toUpperCase().equals("AUTH")) {
sendError("You have to provide your password as "
+ "argument!", this);
} else if (line.toUpperCase().equals("DEREGISTER")) {
if (!authed) {
sendError("You have to be authed to delete "
+ "you password!", this);
Logger.info(this, getNick() + " failed to "
+ "deregister his account (Not "
+ "authed).");
continue;
}
if (!pwds.containsKey(getNick())) {
sendError("There is no password registered "
+ "for your nick!", this);
Logger.info(this, getNick() + " failed to "
+ "deregister his account (No account "
+ "registered).");
continue;
}
Logger.info(this, getNick() + " successfully "
+ "deregistered his account.");
pwds.remove(getNick());
savePwds();
sendDeregistered(this);
} else if (line.toUpperCase().startsWith("ENCRYPT ")) {
String pubKey = line.substring("ENCRYPT ".length());
keyExchange = new DHKeyExchange();
String publicKey = keyExchange.getPublicKey(pubKey);
sendEncrypt(this, publicKey);
Logger.info(this, getNick() + " initiated an "
+ "encrypted connection.");
crypto = new Crypto(keyExchange.getSecretKey());
encrypt = true;
sendEncryptInfo(this);
} else if (line.toUpperCase().equals("ENCRYPT")) {
sendError("You have to provide your public key "
+ "as argument!", this);
} else if (line.toUpperCase().equals("HELP")) {
sendInfo("This server understands the following "
+ "commands:", this);
sendInfo("AUTH ENCRYPT HELP ISAUTHED ISENCRYPTED"
+ " LIST ME NICK TEXT "
+ "TOPIC PING PLAIN PONG PM QUIT REGISTER "
+ "UPTIME", this);
sendInfo("Send HELP <cmd> to get info about the "
+ "command <cmd>.", this);
Logger.info(this, getNick() + " needs help.");
} else if (line.toUpperCase().startsWith("HELP ")) {
String cmd = line.substring("HELP ".length())
.toUpperCase().trim();
if (cmd.toUpperCase().equals("AUTH")) {
Logger.info(this, getNick() + " needs help "
+ "about AUTH.");
sendInfo("AUTH <pwd>", this);
sendInfo("Auth at server. You must use an "
+ "encrypted connection (see HELP "
+ "ENCRYPT) for authing.", this);
} else if (cmd.toUpperCase().equals("ENCRYPT")) {
Logger.info(this, getNick() + " needs help "
+ "about ENCRYPT.");
sendInfo("ENCRYPT <pubkey>", this);
sendInfo("Initialize an encrypted "
+ "connection. <pubkey> is the public "
+ "key of the client.", this);
} else if (cmd.toUpperCase().equals("HELP")) {
Logger.info(this, getNick() + " needs help "
+ "about HELP.");
sendInfo("HELP[ <cmd>]", this);
sendInfo("If <cmd> is omitted print the "
+ "available commands. If <cmd> is "
+ "given print information about the "
+ "command <cmd>.", this);
} else if (cmd.toUpperCase().equals("INDIRECT")) {
Logger.info(this, getNick() + " needs help "
+ "about INDIRECT.");
sendInfo("INDIRECT <text>", this);
sendInfo("Say <text> indirectly to the chat "
+ "like »Bob scratches his head«.",
this);
} else if (cmd.toUpperCase().equals("ISAUTHED")) {
Logger.info(this, getNick() + " needs help "
+ "about ISAUTHED.");
sendInfo("ISAUTHED <nick>", this);
sendInfo("Request if user <nick> is authed "
+ "at server.", this);
} else if (cmd.toUpperCase().equals("ISENCRYPTED")) {
Logger.info(this, getNick() + " needs help "
+ "about ISENCRYPTED.");
sendInfo("ISENCRYPTED <nick>", this);
sendInfo("Request if user <nick> uses an "
+ "encrypted connection.", this);
} else if (cmd.toUpperCase().equals("LIST")) {
Logger.info(this, getNick() + " needs help "
+ "about LIST.");
sendInfo("LIST", this);
sendInfo("Request the user list.", this);
} else if (cmd.toUpperCase().equals("NICK")) {
Logger.info(this, getNick() + " needs help "
+ "about NICK.");
sendInfo("NICK <nick>", this);
sendInfo("Change your nick to <nick>.", this);
} else if (cmd.toUpperCase().equals("TEXT")) {
Logger.info(this, getNick() + " needs help "
+ "about TEXT.");
sendInfo("TEXT <text>", this);
sendInfo("Say <text> to the chat.", this);
} else if (cmd.toUpperCase().equals("TOPIC")) {
Logger.info(this, getNick() + " needs help "
+ "about TOPIC.");
sendInfo("TOPIC[ <topic>]", this);
sendInfo("If <topic> is omitted request the "
+ "current topic. If <topic> is given "
+ "change the topic of the chat to "
+ "<topic>.", this);
} else if (cmd.toUpperCase().equals("PING")) {
Logger.info(this, getNick() + " needs help "
+ "about PING.");
sendInfo("PING", this);
sendInfo("Ask the server if it's still there. "
+ "Will be responded by PONG.", this);
} else if (cmd.toUpperCase().equals("PLAIN")) {
Logger.info(this, getNick() + " needs help "
+ "about PLAIN.");
sendInfo("PLAIN", this);
sendInfo("Ask the server to transmit "
+ "unencrypted again. This command "
+ "must be sent encrypted.", this);
} else if (cmd.toUpperCase().equals("PONG")) {
Logger.info(this, getNick() + " needs help "
+ "about PONG.");
sendInfo("PONG", this);
sendInfo("This should be the reply to PINGs "
+ "from the server.", this);
} else if (cmd.toUpperCase().equals("PM")) {
Logger.info(this, getNick() + " needs help "
+ "about PM.");
sendInfo("PM <nick>: <text>", this);
sendInfo("Send private message <text> only to "
+ "user <nick>.", this);
} else if (cmd.toUpperCase().equals("QUIT")) {
Logger.info(this, getNick() + " needs help "
+ "about QUIT.");
sendInfo("QUIT[ <msg>]", this);
sendInfo("Leave the chat. If <msg> is given "
+ "leave <msg> as quit message.", this);
} else if (cmd.toUpperCase().equals("REGISTER")) {
Logger.info(this, getNick() + " needs help "
+ "about REGISTER.");
sendInfo("REGISTER <pwd>", this);
sendInfo("Register your current nick with "
+ "password <pwd>. This must be done "
+ "using an encrypted connection (see "
+ "HELP ENCRYPT for details).", this);
} else if (cmd.toUpperCase().equals("UPTIME")) {
Logger.info(this, getNick() + " needs help "
+ "about UPTIME.");
sendInfo("UPTIME", this);
sendInfo("Ask how long the server is already "
+ "running.", this);
} else {
Logger.info(this, getNick() + " asks help "
+ "about unknown command " + cmd + ".");
sendError("Unknown command: " + cmd, this);
}
} else if (line.toUpperCase().startsWith("INDIRECT ")) {
sendIndirect(this, line.substring("INDIRECT "
.length()));
Logger.print(this, getNick() + " "
+ line.substring("INDIRECT ".length()));
} else if (line.toUpperCase().equals("INDIRECT")) {
sendError("You have to provide the text you want "
+ "to say indirectly as argument!", this);
} else if (line.toUpperCase().startsWith("ISAUTHED ")) {
String nick = line.substring("ISAUTHED ".length());
Client[] cArray = clients.toArray(new Client[0]);
boolean found = false;
for (Client c : cArray) {
if (c.getNick().equals(nick)) {
Logger.info(this, getNick()
+ " wants to know if " + nick
+ " is authed.");
sendIsAuthed(this, c);
found = true;
break;
}
}
if (!found) {
sendError("There is no user " + nick
+ " online!", this);
Logger.info(this, getNick()
+ " wants to know if " + nick
+ " is authed but there is no " + nick
+ " online.");
}
} else if (line.toUpperCase().equals("ISAUTHED")) {
sendError("You have to provide a user's nickname "
+ "as argument!", this);
} else if (line.toUpperCase()
.startsWith("ISENCRYPTED ")) {
String nick = line.substring("ISENCRYPTED "
.length());
Client[] cArray = clients.toArray(new Client[0]);
boolean found = false;
for (Client c : cArray) {
if (c.getNick().equals(nick)) {
Logger.info(this, getNick()
+ " wants to know if " + nick
+ " uses an encrypted connection.");
sendIsEncrypted(this, c);
found = true;
break;
}
}
if (!found) {
Logger.info(this, getNick()
+ " wants to know if " + nick
+ " uses an encrypted "
+ "connection but there is no " + nick
+ " online.");
sendError("There is no user " + nick
+ " online!", this);
}
} else if (line.toUpperCase().equals("ISENCRYPTED")) {
sendError("You have to provide a user's nickname "
+ "as argument!", this);
} else if (line.toUpperCase().equals("LIST")) {
Logger.info(this, getNick() + " requests the "
+ "user list.");
sendList(this);
} else if (line.toUpperCase().startsWith("NICK ")) {
String newNick = line.substring(5);
if (newNick.equals("")) {
Logger.info(this, getNick() + " tried to "
+ "change his nick to the empty "
+ "string.");
sendError("The empty nick is not allowed! "
+ "Please choose a different nick.",
this);
continue;
}
if (newNick.contains(":")) {
Logger.info(this, getNick() + " tried to "
+ "change the "
+ "nick to a string containing a "
+ "colon which is forbidden.");
sendError("Your nick must not contain any "
+ "colons (\":\")! Please choose a "
+ "different nick.", this);
continue;
}
if (!isInUse(newNick)) {
this.authed = false;
Logger.info(this, getNick() + " is now known "
+ "as " + newNick + ".");
sendNickChange(getNick(), newNick);
this.name = newNick;
} else {
Logger.info(this, getNick()
+ " tried to change the nick to "
+ newNick
+ " but that nick is already in use.");
sendNickInUse(this, newNick);
}
} else if (line.toUpperCase().equals("NICK")) {
sendError("You have to provide your new nickname "
+ "as argument!", this);
} else if (line.toUpperCase().startsWith("TEXT ")) {
Logger.print(this, getNick() + ": "
+ line.substring(5));
sendText(this, line.substring(5));
} else if (line.toUpperCase().equals("TEXT")) {
sendError("You have to provide the text you want "
+ "to say as argument!", this);
} else if (line.toUpperCase().startsWith("TOPIC ")) {
String newTopic = line.substring(6);
Logger.info(this, getNick()
+ " changed the topic to »" + newTopic
+ "«");
sendTopicChange(this, newTopic);
setTopic(newTopic);
topicUser = getNick();
topicTimestamp = System.currentTimeMillis();
} else if (line.toUpperCase().equals("TOPIC")) {
Logger.info(this, getNick() + " wants to know "
+ "the topic.");
sendTopic(this);
} else if (line.toUpperCase().equals("PING")) {
sendPong(this);
} else if (line.toUpperCase().equals("PLAIN")) {
Logger.info(this, getNick()
+ " cancels his encrypted "
+ "connection and switches back to plain "
+ "text.");
sendEndEncrypt(this);
encrypt = false;
crypto = null;
keyExchange = null;
sendEndEncryptInfo(this);
} else if (line.toUpperCase().equals("PONG")) {
ping = false;
} else if (line.toUpperCase().startsWith("PM ")) {
line = line.substring(3);
Client[] cArray;
boolean userFound = false;
String nick = line.substring(0, line.indexOf(':'));
String text = line.substring(line.indexOf(':') + 2,
line.length());
synchronized (clients) {
cArray = clients.toArray(new Client[0]);
}
for (Client c : cArray) {
if (c.getNick().equals(nick)) {
sendPM(this, text, c);
Logger.info(this, getNick()
+ " talks privately to "
+ c.getNick() + ".");
userFound = true;
break;
}
}
if (!userFound) {
Logger.info(this, getNick()
+ " tries to talk privately to " + nick
+ " but there is no user online.");
sendError("There is no user »" + nick
+ "« online.", this);
}
} else if (line.toUpperCase().equals("PM")) {
sendError("Syntax error! Send HELP PM for more "
+ "information!", this);
} else if (line.toUpperCase().startsWith("QUIT ")) {
quit(line.substring(5));
} else if (line.toUpperCase().equals("QUIT")) {
quit(null);
} else if (line.toUpperCase().startsWith("REGISTER ")) {
if (!encrypt) {
Logger.info(this, getNick()
+ " could not register "
+ "(connection not encrypted).");
sendError("For your own safety you have to "
+ "transmit your password via an "
+ "encrypted connection!", this);
sendInfo("You can initialize an encrypted "
+ "connection via the »ENCRYPT« "
+ "command.", this);
continue;
}
if (pwds.containsKey(getNick())) {
Logger.info(this, getNick()
+ " could not register "
+ "(already registered).");
sendError("There already exists a password "
+ "for your nick!", this);
sendInfo("Use DEREGISTER to delete your "
+ "account.", this);
continue;
}
try {
pwds.put(getNick(), Crypto.toSHA(line
.substring("REGISTER ".length())));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
System.exit(-1);
}
Logger.info(this, getNick()
+ " registered successfully.");
authed = true;
savePwds();
sendRegistered(this);
} else if (line.toUpperCase().equals("REGISTER")) {
sendError("You have to provide a password as "
+ "argument!", this);
} else if (line.toUpperCase().equals("UPTIME")) {
Logger.info(this, getNick()
+ " wants to know how long the "
+ "server is already running.");
sendUptime(this);
} else {
Logger.info(this, getNick() + " sent the unknown "
+ "command " + line + ".");
sendError("Unknown command: " + line, this);
}
} else {
kill("Lost connection");
}
}
} catch (IOException e) {
synchronized (clients) {
if (clients.contains(this)) {
Logger.error(this, "Could not read from client socket!"
+ " The client " + getNick()
+ " is kicked from the server now.");
kill("Lost connection");
}
}
}
}
}
/**
* Creates a new talk server listening on port 7886 if no different port is
* specified on the command line.
*
* @param args
* One may specify the port number the server shall run on.
*/
public static void main(String[] args) {
int port = 7886;
for (String arg : args) {
try {
port = Integer.parseInt(arg);
Logger.info(ErkiTalkServer.class, "Using port recognized on "
+ "the command line: " + port + ".");
} catch (NumberFormatException e) {
port = 7886;
Logger.warning(ErkiTalkServer.class, "Could not parse "
+ "command line argument »" + arg + "«!");
}
}
try {
new Thread(new ErkiTalkServer(port)).start();
} catch (Throwable e) {
Logger.error(ErkiTalkServer.class, e);
System.exit(-1);
}
}
}