/* Athena/Aegis Encrypted Chat Platform
* Server.java: Accepts connections, governs user threads (ServerThread instances) and is the gateway to the DB
*
* Copyright (C) 2010 OlympuSoft
* 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.
* 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.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Scanner;
/**
* The main server component. Accepts/manages connections and threads
* @author OlympuSoft
*/
public class Server {
//Change to 1 or 2 for debug output
private static int debug = 0;
//This socket will accept new connection
private static ServerSocket c2ss;
private static ServerSocket c2css;
private static File debugLog;
private static BufferedWriter debugWriter;
/**
* Holds the usernames and hashed passwords read in from the database.
*/
//Creates a SQL connection object. See dbConnect()
private static Connection con = null;
private static String dbUser = "";
private static String dbPass = "";
//Defines which port on which we listen for client
private static int listenPort = 7777;
private static Scanner in = new Scanner(System.in);
/**
* A hashtable that maps users to their server socket
*/
public static Hashtable<String, Socket> userToServerSocket = new Hashtable<String, Socket>();
/**
* A hashtable that maps users to their client socket
*/
public static Hashtable<String, Socket> userToClientSocket = new Hashtable<String, Socket>();
/**
* Server's private RSA key
*/
public static RSAPrivateKeySpec serverPriv;
/**
* Server's public key
*/
public static RSAPublicKeySpec serverPub;
/**
* Starts the server listening
* @param port The port to listen on
* @throws IOException
*/
/*public Server(int port) throws IOException {
listen(port);
}*/
/**
* Gets a connection to the database
* @return The connection to the database
*/
public static Connection dbConnect() {
//Location of the database
String url = "jdbc:mysql://localhost:3306/aegis";
//Database username and password. shhhhh.
String un = dbUser; //Database Username
String pw = dbPass; //Database Password
//Load the JDBC driver. Make sure the mysql jar is in your classpath!
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException ex) {
}
//Connect to the database using the driver
try {
con = DriverManager.getConnection(url, un, pw);
//Return the established connection
return con;
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
/**
* Adds a user and server socket to the hashtable for later reference
* @param username The username
* @param userSocket The socket to map the username to
*/
public static void mapUserServerSocket(String username, Socket userSocket) {
userToServerSocket.put(username, userSocket);
}
/**
* Maps a user and a client socket in the hashtable
* @param username The username
* @param userSocket The socket to map the username to
*/
public static void mapUserClientSocket(String username, Socket userSocket) {
userToClientSocket.put(username, userSocket);
}
/**
* The server listens for client connections, threads them, and loops. Forever.
* @param port Port to listen on
* @throws IOException
*/
@SuppressWarnings("ResultOfObjectAllocationIgnored")
private static void listen(int port) throws IOException {
// Create the ServerSocket
c2ss = new ServerSocket(port);
c2css = new ServerSocket(7778);
//Fetch public and private keys so the threads can deal with encryption
serverPub = RSACrypto.readPubKeyFromFile("keys/Aegis.pub");
serverPriv = RSACrypto.readPrivKeyFromFile("keys/Aegis.priv");
// Tell the world we're ready to go
System.out.println("*******************************************");
System.out.println("** Welcome to Athena Chat Server **");
System.out.println("** Codename: Aegis **");
System.out.println("** **");
System.out.println("** v1.0.1b **");
System.out.println("** **");
System.out.println("** Server accepting connections: **");
System.out.println("** Port 7777 && 7778 **");
System.out.println("** **");
System.out.println("*******************************************");
//Open the log file for writing
openLog(new File("logs/Aegis.txt"));
//Start listening for connections
ListenerThread listener = new ListenerThread(c2ss,c2css);
listener.start();
//The Administration menu
while(true){
System.out.println("\n\nMenu:\nPlease Choose an Action -");
System.out.println("\n1. Change Debug Level\n2. View Log\n3. View Users\n4. Kick User\n5. Exit\n");
System.out.print("?> ");
int answer=in.nextInt();
if(answer==1){
System.out.println("Choose new debug level (0,1,2):");
System.out.print("?> ");
debug = in.nextInt();
}
else if(answer == 2) {
//Need a better way to do this
Runtime.getRuntime().exec("tail ./logs/Aegis.txt");
}
else if(answer == 3) {
//Prints out number of users connected, then usernames
System.out.println(userToClientSocket.size() + " users connected:\n");
Enumeration userEnumeration = userToClientSocket.keys();
//Get the outputStream for each socket and send message
for (Enumeration<?> e = userEnumeration; e.hasMoreElements();) {
System.out.println((String)userEnumeration.nextElement());
}
}
else if(answer ==4) {
in.nextLine();
System.out.println("\nType username of person to kick:");
System.out.print("?> ");
String usernameToKick = in.nextLine();
kickUser(userToServerSocket.get(usernameToKick),userToClientSocket.get(usernameToKick),usernameToKick);
}
else if(answer == 5) {
//Get a reason for the shutdown
System.out.println("\nPlease provide a reason for this shutdown:");
System.out.print("?> ");
in.nextLine();
String message = in.nextLine();
//Inform users of the shutdown, and reason
sendToAll("ServerShutDown",message,null);
try{
//Wait 30 seconds before shutdown
Thread.sleep(30000);
}catch(Exception e){
System.out.println("Could not sleep. Shutting down immediately!");
}
System.out.println("Shutting down...");
System.exit(0);
}
}
}
public static void openLog(File fileName) {
try{
debugLog = fileName;
if (!(debugLog.exists())) {
boolean success = new File("logs").mkdirs();
if (success) {
debugLog.createNewFile();
} else {
debugLog.createNewFile();
}
}
debugWriter = new BufferedWriter(new FileWriter(debugLog));
} catch(Exception e) {
System.out.println("Unable to open log file");
}
}
public static String getDateTime() {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date date = new Date();
return dateFormat.format(date);
}
public static void writeLog(String debugText) {
try{
debugWriter.write(getDateTime() + ": "+debugText+"\r\n");
debugWriter.flush();
} catch(Exception e){
//e.printStackTrace();
System.out.println("Unable to write to log file. Make sure the file is open for writing.");
}
}
public static void closeLog() throws IOException {
debugWriter.close();
}
/**
* Sends a message to every connected user
* @param eventCode What we are talking to them about
* @param message The data
*/
static synchronized void sendToAll(String eventCode, String message,String[] blockList) {
System.gc();
BigInteger eventCodeCipher = new BigInteger(RSACrypto.rsaEncryptPrivate(eventCode, serverPriv.getModulus(), serverPriv.getPrivateExponent()));
BigInteger messageCipher = new BigInteger(RSACrypto.rsaEncryptPrivate(message, serverPriv.getModulus(), serverPriv.getPrivateExponent()));
Enumeration userEnumeration = userToClientSocket.elements();
Enumeration usernameEnumeration = userToClientSocket.keys();
int send=1;
//Get the outputStream for each socket and send message
for (Enumeration<?> e = userEnumeration; e.hasMoreElements();) {
Socket sendToAllSocket = (Socket) e.nextElement();
String userToCheck = (String)usernameEnumeration.nextElement();
writeLog("UserToCheck: "+userToCheck);
if(blockList != null && blockList.length!=0){
writeLog("Blocklist exists");
for(int i=0;i<blockList.length;i++) {
writeLog("Checking: "+blockList[i] + " against " + userToCheck);
if(blockList[i].equals(userToCheck)) send=0;
}}
if(send==1){
try {
DataOutputStream dout = new DataOutputStream(sendToAllSocket.getOutputStream());
dout.writeUTF(eventCodeCipher.toString());
dout.writeUTF(messageCipher.toString());
} catch (IOException ie) {
System.out.println(ie);
}}send=1;
}
}
/**
* Remove a socket (user has failed to login)
* @param servsock The "server" socket to remove
* @param clientsock The "client" socket to remove
*/
static void removeConnection(Socket servsock, Socket clientsock) {
// Debug text
writeLog("Connection Terminated:\n" + servsock + "\n\n");
// Make sure it's closed
try {
servsock.close();
clientsock.close();
} catch (IOException ie) {
writeLog("Error closing " + servsock);
ie.printStackTrace();
}
System.gc();
}
/**
* Remove a socket/outputstream and user/socket relationship (i.e. user disconnects)
* @param servsock The "server" socket
* @param clientsock The "client" socket
* @param uname The username
*/
static void removeConnection(Socket servsock, Socket clientsock, String uname) {
// Debug text
writeLog("User Disconnected: " + uname + "\n\n");
// Remove thread's entries from hashtables
userToServerSocket.remove(uname);
userToClientSocket.remove(uname);
// Make sure the socket is closed
try {
servsock.close();
clientsock.close();
//Sending User Log off message after we close the socket
sendToAll("ServerLogOff", uname,null);
} catch (IOException ie) {
writeLog("Error closing " + servsock);
ie.printStackTrace();
}
System.gc();
}
/**
* Remove a socket/outputstream and user/socket relationship (i.e. user disconnects)
* @param servsock The "server" socket
* @param clientsock The "client" socket
* @param uname The username
*/
static void kickUser(Socket servsock, Socket clientsock, String uname) throws IOException {
// Debug text
writeLog("User Kicked: " + uname + "\n\n");
// Remove thread's entries from hashtables
userToServerSocket.remove(uname);
userToClientSocket.remove(uname);
//Send that user a message notifying him that he's being kicked
DataOutputStream kickDos = new DataOutputStream(clientsock.getOutputStream());
kickDos.writeUTF(ServerThread.encryptServerPrivate("KickMessage"));
kickDos.writeUTF(ServerThread.encryptServerPrivate("You've been kicked by the server."));
// Make sure the socket is closed
try {
servsock.close();
clientsock.close();
//Sending User Log off message after we close the socket
sendToAll("ServerLogOff", uname,null);
} catch (IOException ie) {
writeLog("Error closing " + servsock);
ie.printStackTrace();
}
System.gc();
}
/**
* Read in the usernames from the DB and start listening
* @param args Database credentials
* @throws Exception
*/
public static void main(String args[]) throws Exception {
/*Upon Starting Server
*1. UpdateHashTable
*2. Listen for connections
*3. The Universe collapses in on itself*/
dbUser = args[0];
dbPass = args[1];
// Create a Server object, which will automatically begin accepting connections.
listen(listenPort);
}
}