001: /*- LlamaChatServer.java ------------------------------------------+
002: | |
003: | Copyright (C) 2002-2003 Joseph Monti, LlamaChat |
004: | countjoe@users.sourceforge.net |
005: | http://www.42llamas.com/LlamaChat/ |
006: | |
007: | This program is free software; you can redistribute it and/or |
008: | modify it under the terms of the GNU General Public License |
009: | as published by the Free Software Foundation; either version 2 |
010: | of the License, or (at your option) any later version |
011: | |
012: | This program is distributed in the hope that it will be useful, |
013: | but WITHOUT ANY WARRANTY; without even the implied warranty of |
014: | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
015: | GNU General Public License for more details. |
016: | |
017: | A copy of the GNU General Public License may be found in the |
018: | installation directory named "GNUGPL.txt" |
019: | |
020: +-----------------------------------------------------------------+
021: */
022:
023: package server;
024:
025: import java.io.IOException;
026: import javax.net.ssl.*;
027: import java.util.Calendar;
028: import java.util.Enumeration;
029: import java.util.Hashtable;
030: import java.util.LinkedList;
031: import java.io.BufferedOutputStream;
032: import java.io.File;
033: import java.io.FileNotFoundException;
034: import java.io.FileOutputStream;
035: import java.io.PrintWriter;
036: import java.io.BufferedReader;
037: import java.io.FileReader;
038: import org.xml.sax.*;
039: import org.xml.sax.helpers.DefaultHandler;
040: import javax.xml.parsers.SAXParserFactory;
041: import javax.xml.parsers.ParserConfigurationException;
042: import javax.xml.parsers.SAXParser;
043:
044: import common.*;
045: import common.sd.*;
046:
047: /* -------------------- JavaDoc Information ----------------------*/
048: /**
049: * The main class for the LlamaChat server
050: * @author Joseph Monti <a href="mailto:countjoe@users.sourceforge.net">countjoe@users.sourceforge.net</a>
051: * @version 0.8
052: */
053: public final class LlamaChatServer extends Thread {
054:
055: private static LlamaChatServer listeningServer;
056: public static int PORT = 42412;
057: public static String sysLogFile = "llamachat.log";
058: public static String adminPass = "llamachat";
059: public static LinkedList connectingUsers;
060: public static Hashtable connectedUsers;
061: public static boolean running;
062: public static boolean allowAdmin = true;
063: private static PrintWriter sysLogOut;
064: public static String chatLogPath = ".";
065: private static Hashtable channelFiles;
066: public static String userExportFile = null;
067: public static ChannelManager channels;
068: public static String welcomeMessage = null;
069: public static String serverConfigFile = "llamachatconf.xml";
070:
071: /**
072: * called when a new user has connected.
073: * @param s the secure socket that the user connected on
074: */
075: synchronized public void newUser(SSLSocket s) {
076: ClientConnection cc = new ClientConnection(this , s);
077: connectingUsers.add(cc);
078: }
079:
080: /**
081: * finalizes the connection to the client by setting its username
082: * and updating all lists of users by sending the new user to all the
083: * connected users and sending the user list to the new user, also
084: * checks the validity of the username
085: * @param uname the desired user name for the new user
086: * @param cc the object representing the connecting user
087: * @return true on success, false otherwise
088: */
089: synchronized public boolean finalizeUser(String uname,
090: ClientConnection cc) {
091: if (connectedUsers.containsKey(uname)
092: || connectedUsers.contains(cc)) {
093: cc
094: .writeObject(new SD_Error(
095: "Already a user of that name or "
096: + "already connected. \nType \\rename <new name> to "
097: + "choose a different name"));
098: log(cc, "failed [duplicate exists]");
099: return false;
100: } else if (uname.length() > 10) {
101: cc.writeObject(new SD_Error("Username too long.\n"
102: + "Type \\rename <new name> to choose "
103: + "a different name"));
104: log(cc, "failed [bad name]");
105: return false;
106: } else if (uname.equals("server")
107: || !uname.matches("[\\w_-]+?")) {
108: cc.writeObject(new SD_Error("Invalid username " + uname
109: + ".\n"
110: + "Type \\rename <new name> to choose "
111: + "a different name"));
112: log(cc, "failed [bad name]");
113: return false;
114: } else if (connectingUsers.remove(cc)) {
115: cc.writeObject(new SD_ServerCap(SD_ServerCap.T_CREATE,
116: new Character(channels.allowUserChannels)));
117: if (welcomeMessage != null) {
118: cc.writeObject(new SD_Chat("server", welcomeMessage));
119: }
120: cc.writeObject(new SD_Channel(false, cc.channel, null));
121: sendUserList(cc);
122: Enumeration e = channels.enumerate();
123: while (e.hasMoreElements()) {
124: String channel = (String) e.nextElement();
125: cc.writeObject(new SD_Channel(true, channel, channels
126: .channelHasPass(channel)));
127: }
128: connectedUsers.put(uname, cc);
129: broadcast(new SD_UserAdd(uname), uname, cc.channel);
130: updateUserExport();
131: log(cc, "connected as " + uname);
132: return true;
133: }
134:
135: cc.writeObject(new SD_Error(
136: "invalid connection procedure, please try again"));
137: return false;
138: }
139:
140: /**
141: * sends the current user listing to the specified user
142: * @param cc the user requesting the list
143: */
144: synchronized public void sendUserList(ClientConnection cc) {
145: Enumeration e = connectedUsers.keys();
146: while (e.hasMoreElements()) {
147: String un = (String) e.nextElement();
148: if (cc.name.equals(un)) {
149: continue;
150: }
151: ClientConnection o = (ClientConnection) connectedUsers
152: .get(un);
153: if (o.channel.equals(cc.channel)) {
154: cc.writeObject(new SD_UserAdd(un));
155: if (o.isAdmin()) {
156: cc.writeObject(new SD_AdminAdd(un));
157: }
158: }
159: }
160: cc.writeObject(new SD_UserAdd(null)); // send End of List
161: }
162:
163: /**
164: * sends the specified SocketData to the client, to
165: * @param sd the SocketData object to be sent
166: * @param to the user to be sent the data
167: * @return status of the sendTo
168: */
169: synchronized public boolean sendTo(SocketData sd, String to) {
170: ClientConnection o = (ClientConnection) connectedUsers.get(to);
171: if (o != null) {
172: o.writeObject(sd);
173: return true;
174: } else {
175: return false;
176: }
177: }
178:
179: /**
180: * broadcases a message to all connected user except from
181: * @param sd the SocketData object to be sent
182: * @param from the user to be avoided when sending data
183: * (usually the user sending the data)
184: */
185: synchronized public void broadcast(SocketData sd, String from) {
186: Enumeration e = connectedUsers.keys();
187: while (e.hasMoreElements()) {
188: String to = (String) e.nextElement();
189: if (!to.equals(from)) {
190: ClientConnection o = (ClientConnection) connectedUsers
191: .get(to);
192: o.writeObject(sd);
193: }
194: }
195: }
196:
197: /**
198: * broadcases a message to all connected user except from
199: * and is a member of c
200: * @param sd the SocketData object to be sent
201: * @param from the user to be avoided when sending data
202: * (usually the user sending the data)
203: * @param c the channel the user is connected to
204: */
205: synchronized public void broadcast(SocketData sd, String from,
206: String c) {
207: Enumeration e = connectedUsers.keys();
208: while (e.hasMoreElements()) {
209: String to = (String) e.nextElement();
210: if (!to.equals(from)) {
211: ClientConnection o = (ClientConnection) connectedUsers
212: .get(to);
213: if (o.channel.equals(c)) {
214: o.writeObject(sd);
215:
216: }
217: }
218: }
219: }
220:
221: /**
222: * sends a SD_UserDel object to all users announcing that un has left
223: * the building
224: * @param cc the user leaving
225: */
226: synchronized public void delete(ClientConnection cc) {
227: broadcast(new SD_UserDel(cc.name), null, cc.channel);
228: }
229:
230: /**
231: * Removes a user from the list of connected users and calls delete
232: * @param cc the associated ClientConnection.
233: * @see #delete(ClientConnection cc)
234: */
235: synchronized public void kill(ClientConnection cc) {
236: try {
237: if (cc.name != null && connectedUsers.remove(cc.name) == cc) {
238: log(cc, cc.name + " disconnected");
239: updateUserExport();
240: if (channels.userDel(cc.channel)) {
241: broadcast(new SD_Channel(true, cc.channel, null),
242: null);
243: chatLog(cc, false);
244: }
245: delete(cc);
246: }
247: } catch (NullPointerException e) {
248: e.printStackTrace();
249: if (cc != null) {
250: log("null pointer on kill: " + cc.name + " ("
251: + cc.channel + ")");
252: } else {
253: log("got sent a null cc");
254: }
255: }
256: }
257:
258: /**
259: * gets the current date/time of the form MM/DD/YY HH:MM:SS
260: * used for loggin purposes
261: * @return a string specifying the current date/time
262: */
263: public static String getTimestamp() {
264: Calendar rightNow = Calendar.getInstance();
265: return rightNow.get(Calendar.MONTH) + "/"
266: + rightNow.get(Calendar.DATE) + "/"
267: + rightNow.get(Calendar.YEAR) + " "
268: + rightNow.get(Calendar.HOUR_OF_DAY) + ":"
269: + rightNow.get(Calendar.MINUTE) + ":"
270: + rightNow.get(Calendar.SECOND);
271: }
272:
273: /**
274: * writes to the servers log file preceded by a timestamp
275: * @param s the string to be logged
276: */
277: synchronized private void log(String s) {
278: if (sysLogOut != null) {
279: sysLogOut.println(getTimestamp() + " - " + s);
280: sysLogOut.flush();
281: }
282: }
283:
284: /**
285: * a public method used by clients to send log data
286: * @param cc the client sending the log, used to identify
287: the logger in the log
288: * @param s the string to be logged
289: */
290: public void log(ClientConnection cc, String s) {
291: log(cc.ip + " - " + s);
292: }
293:
294: /**
295: * manages the chat log
296: * <br><br><i>do something about channels</i>
297: * @param cc the client attempting to change the log status. used to
298: * determine what channel to work with, if null close all
299: * @param start enables chat logging when true, stops otherwise
300: * @return true on succes
301: */
302: synchronized public boolean chatLog(ClientConnection cc,
303: boolean start) {
304: if (cc == null) {
305: Enumeration e = channelFiles.keys();
306: while (e.hasMoreElements()) {
307: String name = (String) e.nextElement();
308: ChatFileItem item = (ChatFileItem) channelFiles
309: .get(name);
310: closeLog(null, item);
311: }
312: return true;
313: }
314: ChatFileItem currentWriter = (ChatFileItem) channelFiles
315: .get(cc.channel);
316: if (start) {
317: if (currentWriter != null && currentWriter.logging) {
318: cc.writeObject(new SD_Error("already logging "
319: + cc.channel));
320: return false;
321: }
322: if (currentWriter == null) {
323: currentWriter = new ChatFileItem();
324: channelFiles.put(cc.channel, currentWriter);
325: }
326: String fname = chatLogPath
327: + System.getProperty("file.separator") + "chat-"
328: + cc.channel + ".log";
329: try {
330: currentWriter.chatOut = new PrintWriter(
331: new BufferedOutputStream(new FileOutputStream(
332: new File(fname), true)));
333: currentWriter.chatOut.println("log started by "
334: + (cc != null ? cc.name : "server") + " at "
335: + getTimestamp());
336: currentWriter.chatOut
337: .println("====================================================");
338: currentWriter.chatOut.flush();
339: currentWriter.logging = true;
340: } catch (FileNotFoundException e) {
341: cc.writeObject(new SD_Error("error opening " + fname));
342: listeningServer.running = false;
343: return false;
344: }
345: return true;
346: }
347:
348: return closeLog(cc, currentWriter);
349: }
350:
351: /**
352: * utility method to close a specified log
353: * @param cc the client attempting to close
354: * @param currentWriter the ChatFileItem to close
355: * @return true on success, false on failure
356: */
357: private boolean closeLog(ClientConnection cc,
358: ChatFileItem currentWriter) {
359: if (currentWriter == null || currentWriter.chatOut == null) {
360: //cc.writeObject(new SD_Error("not logging"));
361: return false;
362: }
363: currentWriter.chatOut
364: .println("====================================================");
365: currentWriter.chatOut.println("log stopped by "
366: + (cc != null ? cc.name : "server") + " at "
367: + getTimestamp());
368: currentWriter.chatOut
369: .println("====================================================");
370: currentWriter.chatOut.flush();
371: currentWriter.chatOut.close();
372: currentWriter.chatOut = null;
373: currentWriter.logging = false;
374: if (cc != null) {
375: channelFiles.remove(cc.channel);
376: }
377: return true;
378: }
379:
380: /**
381: * logs the specified chat message
382: * @param cc the client chatting
383: * @param message the message to be sent
384: */
385: synchronized public void chatLog(ClientConnection cc, String message) {
386: ChatFileItem currentWriter = (ChatFileItem) channelFiles
387: .get(cc.channel);
388: if (currentWriter != null && currentWriter.logging
389: && currentWriter.chatOut != null) {
390: currentWriter.chatOut.println(cc.name + ": " + message);
391: currentWriter.chatOut.flush();
392: }
393: }
394:
395: /**
396: * allows for the user listing (newline separated) to be exported
397: * to the filesystem, should be called whenever users are added or
398: * removed or renamed
399: * @return true on succes, false otherwise
400: */
401: synchronized static public boolean updateUserExport() {
402: if (userExportFile == null) {
403: return false;
404: }
405: try {
406: PrintWriter pw = new PrintWriter(new BufferedOutputStream(
407: new FileOutputStream(new File(userExportFile),
408: false)));
409: Enumeration e = connectedUsers.keys();
410: int i;
411: for (i = 0; e.hasMoreElements(); i++) {
412: String usr = (String) e.nextElement();
413: ClientConnection o = (ClientConnection) connectedUsers
414: .get(usr);
415: pw.println(usr + " (" + o.channel + ")");
416: }
417:
418: if (i == 0) {
419: pw.println("(no connected users)");
420: }
421:
422: pw.close();
423: } catch (FileNotFoundException e) {
424: System.err.println("unable to access userExportFile: "
425: + userExportFile);
426: userExportFile = null;
427: }
428: return true;
429: }
430:
431: /**
432: * creates a new channel, assuming the channel can be created
433: * @param name the name of the channel to be craeted
434: * @param pass the password for the channel
435: * @param cc the client requesting the channel
436: * @return true if created
437: */
438: synchronized public String newChannel(String name, String pass,
439: ClientConnection cc) {
440: String ret;
441: if ((ret = channels.addUserChannel(name, pass, cc)) != null) {
442: return ret;
443: }
444: broadcast(
445: new SD_Channel(true, name, (pass == null ? null : "")),
446: null);
447: return null;
448: }
449:
450: /**
451: * checks to see if the specified channel exists
452: * @param name the name of the channel
453: * @return true if name exists exists
454: */
455: synchronized public boolean channelExists(String name) {
456: return channels.channelExists(name);
457: }
458:
459: /**
460: * the main loop to recieving incoming clients listens on a secure
461: * server socket creates a shutdown hook to catch a server kill
462: * and exit gracefully
463: */
464: public void run() {
465: Runtime.getRuntime().addShutdownHook(new ShutdownThread(this ));
466: try {
467: SSLServerSocketFactory factory = (SSLServerSocketFactory) SSLServerSocketFactory
468: .getDefault();
469: SSLServerSocket sslIncoming = (SSLServerSocket) factory
470: .createServerSocket(PORT);
471: String[] enabledCipher = sslIncoming
472: .getSupportedCipherSuites();
473: sslIncoming.setEnabledCipherSuites(enabledCipher);
474:
475: log("Server Started");
476: while (running) {
477: SSLSocket s = (SSLSocket) sslIncoming.accept();
478: newUser(s);
479: }
480: } catch (IOException e) {
481: System.out.println("Error: " + e);
482: }
483: log("Server Stopped");
484: }
485:
486: /**
487: * a useless constructor
488: */
489: LlamaChatServer() {
490: running = true;
491: }
492:
493: /**
494: * main method for class; initializes lists and system log file
495: * @param args the command line arguments
496: */
497: public static void main(String args[]) {
498: listeningServer = new LlamaChatServer();
499: //listeningServer.running = true;
500:
501: connectingUsers = new LinkedList();
502: connectedUsers = new Hashtable();
503: channels = new ChannelManager();
504: channelFiles = new Hashtable();
505:
506: try {
507: DefaultHandler handler = new ConfigParser(listeningServer);
508: SAXParserFactory factory = SAXParserFactory.newInstance();
509: SAXParser saxParser = factory.newSAXParser();
510: saxParser.parse(new File(serverConfigFile), handler);
511: } catch (Throwable t) {
512: System.err.println("error parsing " + serverConfigFile);
513: running = false;
514: }
515:
516: try {
517: sysLogOut = new PrintWriter(new BufferedOutputStream(
518: new FileOutputStream(new File(sysLogFile), true)));
519: } catch (FileNotFoundException e) {
520: System.err.println(sysLogFile + " not found.");
521: running = false;
522: }
523:
524: updateUserExport();
525:
526: listeningServer.start();
527: try {
528: listeningServer.join();
529: } catch (InterruptedException e) {
530: }
531: }
532:
533: /**
534: * A class to enable gracefull shutdown when unexpectadly killed
535: */
536: public class ShutdownThread extends Thread {
537: private LlamaChatServer lcs;
538:
539: ShutdownThread(LlamaChatServer l) {
540: lcs = l;
541: }
542:
543: public void run() {
544: log("Server Stopped");
545: sysLogOut.close();
546: chatLog(null, false);
547: }
548: }
549:
550: /**
551: * a class to hold logging information on a Hashtable
552: */
553: private class ChatFileItem {
554: public boolean logging;
555: public PrintWriter chatOut;
556:
557: ChatFileItem() {
558: logging = false;
559: chatOut = null;
560: }
561: }
562: }
|