/**
* @(#)gnsd.java 1.0.1 98/12/20
*
* Copyright (C) 1998 David E. Wexler
*
* 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; either version 2
* of the License, or (at your option) any later version. See
* license.txt for a full copy of the GNU GPL.
*
* 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.
*
* To Contact the author send e-mail to vagabond@netdragon.com or send
* snailmail to: 511 Bobcat Ct, Punta Gorda, FL 3398.
*/
import java.net.*;
import java.io.*;
import java.util.*;
import logger;
import gnsd;
import Group;
/**
* User - Stores user data and provides the user with an connection
* to interact with.
*
* Each users is given it's own thread to run in. If the user fails to
* send data to the server in 5 mins the connection will automatically close.
* This will handle all the commands sent from the client, and handle
* sending all the data from the server back to the client.
*
* @author David Wexler (vagabond@netdragons.com)
* @version 1.0.1
* @since gnsd.1.0.1
*/
public class User extends Thread {
/**
* Open socket connecting the client and server. This is used for all
* incoming and out going traffic.
*/
private Socket soc;
/**
* Allows the server to communicate with the client.
*/
private DataInputStream in;
/**
* Allows the client to communicate with the server.
*/
private DataOutputStream out;
/**
* Used to log major commands to PATH/log/access_log. This
* will log @connect, @login, @join, @joinnew, @list, @part, and @logout.
* Logs are timestamped via time_t, information about the user's host,
* command, and other useful information is given.
*/
private logger log;
/**
* The users ID number. This is set to -1 if the user is not authenticated.
*/
private int id = -1;
/**
* The users name or handle. This will be null until the user has
* authenticated.
*/
private String name;
/**
* The game or client ID that the user is using. This will be null until the
* user has authenticated. Used so a number of different clients can connet
* to the server without them conflicting.
*/
private int gameID;
/**
* Used to hold information set by @setmode. This can store information
* to be transmited on joining the network, this can be used for control
* reasons, or just to transmit extra data.
*/
private String mode = "";
/**
* Type of incoming data. Used to see if there is a leading @ or #.
*/
private char type;
/**
* Instance of the server so we can perform server level actions. This
* includes joining groups, getting group lists, getting user counts, etc.
*/
private gnsd server;
/**
* Instnace of the current users group. This is so the user can send
* data to the group, can leave the group, etc. This will be null until
* the user has joined a channel.
*/
private Group myGroup;
/**
* Indicated if the players connection is active.
*/
private boolean active = false;
/**
* Creates a new instance of a User. Sets up the input stream and output
* stream, through buffers, from the client's socket. Also sets the timeout
* to 5 minutes.
*
* @param s the client's socket.
* @param l Instance of logger, use to log commands.
* @param g Instance of the server so we can perform server
* level actions.
* @since gnsd.1.0.1
*/
public User (Socket s, logger l, gnsd g) throws IOException {
soc = s;
in = new DataInputStream(new BufferedInputStream(s.getInputStream()));
out = new DataOutputStream(new BufferedOutputStream(s.getOutputStream()));
log = l;
server = g;
active = true;
}
/**
* Handles the execution of the users thread. We sit in an infinate loop
* and read data from the input stream. The thread is only borken when
* the client's connection is broken. Server is hardcoded to read UTF
* data only.
*
* @since gnsd.1.0.1
*/
public void run () {
try {
while (true) {
handle(in.readUTF());
}
} catch (IOException ex) {
// No need to do anything!
} finally {
if (active)
logout("Connection Timed Out.");
}
}
/**
* Sends a message to the client. Data is sent to the client in UTF format.
*
* @param msg the message to send.
* @since gnsd.1.0.1
*/
public void send(String msg) {
try {
out.writeUTF(msg);
out.flush();
} catch (IOException ex) {
if (log.debug())
ex.printStackTrace();
}
}
/**
* Allows the user to join a group. A User can only be in one group at a
* time, so the server is carefully crafted to make sure part is called
* before we join a group.
*
* @param g the group to join.
* @set #part()
* @since gnsd.1.0.1
*/
public void join(Group g) {
myGroup = g;
send("@|chanjoin|" + g.getTitle() + "`" + g.getMode());
send("@|who|" + myGroup.getUserList());
myGroup.addUser(this);
}
/**
* Gets the users name.
*
* @return the name of the user.
* @since gnsd.1.0.1
*/
public String getUserName() {
return name;
}
/**
* Gets the host address of the client's socket. This is used for logging.
* Later versions of the server may allow a @whois command so direct client
* to client communication can be used.
*
* @return the host address of the client.
* @since gnsd.1.0.1
*/
public String getAddress() {
if (soc != null)
return soc.getInetAddress().getHostName();
else
return "";
}
/**
* Gets the users ID number.
*
* @return the users ID number.
* @since gnsd.1.0.1
*/
public int getID() {
return id;
}
/**
* Gets the users mode.
*
* @return the users mode.
* @since gnsd.1.0.2
*/
public String getMode() {
return mode;
}
/**
* Gets the users Game/Client ID number.
*
* @return the users game/client ID number.
* @since gnsd.1.0.1
*/
public int getGameID() {
return gameID;
}
/**
* Handles input from the client when it's presented with data. The
* String needs to begin with @ (a command) or # (broadcast) for the
* server to handle the information. Depending on which type of data
* is sent the proper handling function is called. If the String does
* not start with a known char, an error is returned to the client.
*
* @see #handleCommand(java.lang.String)
* @see #handleData(java.lang.String)
* @since gnsd.1.0.1
*/
private void handle(String message) {
type = message.charAt(0);
if (type == '#')
handleData(new String(message.toCharArray(), 1, message.length()-1));
else if (type == '@')
handleCommand(new String(message.toCharArray(), 1, message.length()-1));
else
send("@|ERROR|Malformed Protocol \"" + message + "\"");
}
/**
* Handles all the commands the client could posible send. For a list of
* all clients visit http://www.netdragons.com/games/24/gns/commands/. If
* the command isn't understood, an error is returned to the client.
*
* @param data data given from the client.
* @since gnsd.1.0.1
*/
private void handleCommand(String data) {
StringTokenizer Token = new StringTokenizer(data, "|", false);
String cmd = Token.nextToken();
if (cmd.compareTo("gameCmd") == 0)
sendGameCommand(Token.nextToken());
else if (cmd.compareTo("privmsg") == 0 && myGroup != null)
sendPrivMessage(Token.nextToken());
else if (cmd.compareTo("login") == 0)
login(Token.nextToken());
else if (cmd.compareTo("part") == 0)
part();
else if (cmd.compareTo("setmode") == 0)
mode = Token.nextToken();
else if (cmd.compareTo("join") == 0)
join(toInt(Token.nextToken()));
else if (cmd.compareTo("joinnew") == 0)
joinNew(Token.nextToken());
else if (cmd.compareTo("list") == 0)
server.getList(gameID, this);
else if (cmd.compareTo("who") == 0 && myGroup != null)
send("@|who|" + myGroup.getUserList());
else if (cmd.compareTo("logout") == 0)
logout("User Quit");
else if (cmd.compareTo("count") == 0)
send("@|count|" + server.getUserCount());
else if (cmd.compareTo("oneway") == 0 && myGroup != null)
myGroup.setBroadcastType(false);
else if (cmd.compareTo("twoway") == 0 && myGroup != null)
myGroup.setBroadcastType(true);
else if (cmd.compareTo("transid") == 0 && myGroup != null)
myGroup.useName = false;
else if (cmd.compareTo("transname") == 0 && myGroup != null)
myGroup.useName = true;
else
send("@|ERROR|Unknown Command");
}
/**
* Handles broadcasting of data to all members of a group. If the
* user is not authenticated, or is not in a group, and error is returned
* to the client.
*
* @param data the data to broadcast.
* @since gnsd.1.0.1
*/
private void handleData(String data) {
if (id == -1)
send("@|ERROR|User Not Authenticated");
else if (myGroup == null)
send("@|ERROR|Cannot send message, not in a group!");
else
if (myGroup.useName)
myGroup.sendAll("#|" + name + "|" + data, id);
else
myGroup.sendAll("#|" + id + "|" + data, id);
}
/**
* Broadcast game information to all clients in a group. This is used
* to transmit informational data, such as a ship type, weapon change, etc.
* most data will be send through handleData.
*
* @param data the data to broadcast.
* @see #handleData(java.lang.String)
* @since gnsd.1.0.1
*/
private void sendGameCommand(String data) {
if (myGroup == null || id == -1)
send("@|ERROR|Cannot send Game Command!");
else
myGroup.sendAll("@|gameCmd|" + name + "`" + data, -1);
}
/**
* Sends a private message to a user. This only works when the user is
* in the same group that you are.
*
* @param data the data to private message
* @since gnsd.1.0.1
*/
private void sendPrivMessage(String data) {
StringTokenizer Token = new StringTokenizer(data, "`", false);
myGroup.sendPriv(Token.nextToken(), Token.nextToken());
}
/**
* Uses the server level commands to join a group. If the user is not
* authenticated, an error is returned to the client. If the group
* does not exist, an error is returned to the client. To make sure
* the user is not in a group, the part() function is called.
*
* @param cID group ID to join.
* @see #part()
* @since gnsd.1.0.1
*/
private void join(int cID) {
if (id == -1)
send("@|ERROR|Cannot join channel until Authenticated");
else if (server.checkGroup(cID)) {
part();
server.join(cID, this);
} else
send("@|ERROR|Cannot join unformed channel with that command.");
}
/**
* Allows the user to make a new group. This works much the same
* way as the join command, and returns errors the same. To make
* sure we don't create two group with the same ID, we check to
* make sure the group doesn't exist. If it does already exist, it is
* just like using join(id);
*
* @param data new channel information
* @see #join(int)
* @since gnsd.1.0.1
*/
private void joinNew(String data) {
StringTokenizer Token = new StringTokenizer(data, "`", false);
int cID = toInt(Token.nextToken());
if (id == -1)
send("@|ERROR|Cannot join channel until Authenticated");
if (server.checkGroup(cID)) {
part();
server.join(cID, this);
} else {
part();
server.joinNew(cID, gameID, Token.nextToken(), Token.nextToken(), this);
}
}
/**
* Allows a user to leave a channel. Makes sure that the user is in a group
* to begin with, and if they are, they are removed from it. myGroup is
* set to null to finish the operation.
*
* @see #myGroup
* @since gnsd.1.0.1
*/
private synchronized void part() {
if (myGroup != null) {
log.print(getAddress() + " @part("+ myGroup.getID() + ") " + id + " 100");
myGroup.removeUser(this);
myGroup = null;
}
}
/**
* Allows the user to Login to the network and become authinticated. Uses
* a server level command, and checks to make sure the name is valid, i.e.
* not "logout". If authintication fails, an error is returned to the
* client and they are disconnected. If the login work, id, gameID, and
* and name are all set.
*
* @param data user data, "userID`password`gameID"
* @see id
* @see gameID
* @see name
* @since gnsd.1.0.1
*/
private void login(String data) {
StringTokenizer Token = new StringTokenizer(data, "`", false);
name = server.authenticate (id = toInt(Token.nextToken()),
Token.nextToken());
if (name.compareTo("logout") == 0) {
id = -1;
logout("User Authentication Failed!");
} else {
gameID = toInt(Token.nextToken());
send("@|login|" + gameID);
server.userCount(+1);
log.print(getAddress() + " @login(" + gameID + ") 100");
server.join(gameID, this);
}
}
/**
* Causes the client connection to be closed. If posible the client
* will be notified that the connection is being closed.
*
* @param msg Message to send to client.
* @since gnsd.1.0.1
*/
private void logout(String msg) {
if (id == -1)
log.print(getAddress() + " @logout 100");
else {
server.userCount(-1);
part();
log.print(getAddress() + " @logout " + id + " 100");
}
send("@|logout|" + msg);
try {
soc.close();
soc = null;
} catch (IOException ex) {
if (log.debug())
ex.printStackTrace();
}
active = false;
stop();
}
/**
* Converts a String to an int;
*
* @param s The String to be converted
* @return an int value from s
* @since gnsd.1.0.1
*/
private int toInt(String s) {
Integer i = new Integer(s);
return i.intValue();
}
}