KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > server > LlamaChatServer


1 /*- LlamaChatServer.java ------------------------------------------+
2  | |
3  | Copyright (C) 2002-2003 Joseph Monti, LlamaChat |
4  | countjoe@users.sourceforge.net |
5  | http://www.42llamas.com/LlamaChat/ |
6  | |
7  | This program is free software; you can redistribute it and/or |
8  | modify it under the terms of the GNU General Public License |
9  | as published by the Free Software Foundation; either version 2 |
10  | of the License, or (at your option) any later version |
11  | |
12  | This program is distributed in the hope that it will be useful, |
13  | but WITHOUT ANY WARRANTY; without even the implied warranty of |
14  | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15  | GNU General Public License for more details. |
16  | |
17  | A copy of the GNU General Public License may be found in the |
18  | installation directory named "GNUGPL.txt" |
19  | |
20  +-----------------------------------------------------------------+
21  */

22
23 package server;
24
25 import java.io.IOException JavaDoc;
26 import javax.net.ssl.*;
27 import java.util.Calendar JavaDoc;
28 import java.util.Enumeration JavaDoc;
29 import java.util.Hashtable JavaDoc;
30 import java.util.LinkedList JavaDoc;
31 import java.io.BufferedOutputStream JavaDoc;
32 import java.io.File JavaDoc;
33 import java.io.FileNotFoundException JavaDoc;
34 import java.io.FileOutputStream JavaDoc;
35 import java.io.PrintWriter JavaDoc;
36 import java.io.BufferedReader JavaDoc;
37 import java.io.FileReader JavaDoc;
38 import org.xml.sax.*;
39 import org.xml.sax.helpers.DefaultHandler JavaDoc;
40 import javax.xml.parsers.SAXParserFactory JavaDoc;
41 import javax.xml.parsers.ParserConfigurationException JavaDoc;
42 import javax.xml.parsers.SAXParser JavaDoc;
43
44 import common.*;
45 import common.sd.*;
46
47 /* -------------------- JavaDoc Information ----------------------*/
48 /**
49  * The main class for the LlamaChat server
50  * @author Joseph Monti <a HREF="mailto:countjoe@users.sourceforge.net">countjoe@users.sourceforge.net</a>
51  * @version 0.8
52  */

53 public final class LlamaChatServer extends Thread JavaDoc {
54
55     private static LlamaChatServer listeningServer;
56     public static int PORT = 42412;
57     public static String JavaDoc sysLogFile = "llamachat.log";
58     public static String JavaDoc adminPass = "llamachat";
59     public static LinkedList JavaDoc connectingUsers;
60     public static Hashtable JavaDoc connectedUsers;
61     public static boolean running;
62     public static boolean allowAdmin = true;
63     private static PrintWriter JavaDoc sysLogOut;
64     public static String JavaDoc chatLogPath = ".";
65     private static Hashtable JavaDoc channelFiles;
66     public static String JavaDoc userExportFile = null;
67     public static ChannelManager channels;
68     public static String JavaDoc welcomeMessage = null;
69     public static String JavaDoc serverConfigFile = "llamachatconf.xml";
70
71     /**
72      * called when a new user has connected.
73      * @param s the secure socket that the user connected on
74      */

75     synchronized public void newUser(SSLSocket s) {
76         ClientConnection cc = new ClientConnection(this, s);
77         connectingUsers.add(cc);
78     }
79
80     /**
81      * finalizes the connection to the client by setting its username
82      * and updating all lists of users by sending the new user to all the
83      * connected users and sending the user list to the new user, also
84      * checks the validity of the username
85      * @param uname the desired user name for the new user
86      * @param cc the object representing the connecting user
87      * @return true on success, false otherwise
88      */

89     synchronized public boolean finalizeUser(String JavaDoc uname,
90                                             ClientConnection cc) {
91         if (connectedUsers.containsKey(uname) ||
92                                     connectedUsers.contains(cc)) {
93             cc.writeObject(new SD_Error("Already a user of that name or " +
94                     "already connected. \nType \\rename &lt;new name&gt; to " +
95                     "choose a different name"));
96             log(cc, "failed [duplicate exists]");
97            return false;
98         } else if (uname.length() > 10) {
99             cc.writeObject(new SD_Error("Username too long.\n" +
100                             "Type \\rename &lt;new name&gt; to choose " +
101                             "a different name"));
102             log(cc, "failed [bad name]");
103             return false;
104         } else if (uname.equals("server") || !uname.matches("[\\w_-]+?")) {
105             cc.writeObject(new SD_Error("Invalid username " + uname + ".\n" +
106                             "Type \\rename &lt;new name&gt; to choose " +
107                             "a different name"));
108             log(cc, "failed [bad name]");
109             return false;
110         } else if (connectingUsers.remove(cc)) {
111             cc.writeObject(new SD_ServerCap(SD_ServerCap.T_CREATE,
112                             new Character JavaDoc(channels.allowUserChannels)));
113             if (welcomeMessage != null) {
114                 cc.writeObject(new SD_Chat("server", welcomeMessage));
115             }
116             cc.writeObject(new SD_Channel(false, cc.channel, null));
117             sendUserList(cc);
118             Enumeration JavaDoc e = channels.enumerate();
119             while (e.hasMoreElements()) {
120                 String JavaDoc channel = (String JavaDoc) e.nextElement();
121                 cc.writeObject(new SD_Channel(true, channel,
122                         channels.channelHasPass(channel)));
123             }
124             connectedUsers.put(uname, cc);
125             broadcast(new SD_UserAdd(uname), uname, cc.channel);
126             updateUserExport();
127             log(cc, "connected as " + uname);
128             return true;
129         }
130
131         cc.writeObject(new SD_Error("invalid connection procedure, please try again"));
132         return false;
133     }
134
135     /**
136      * sends the current user listing to the specified user
137      * @param cc the user requesting the list
138      */

139     synchronized public void sendUserList(ClientConnection cc) {
140         Enumeration JavaDoc e = connectedUsers.keys();
141         while (e.hasMoreElements()) {
142             String JavaDoc un = (String JavaDoc) e.nextElement();
143             if (cc.name.equals(un)) {
144                 continue;
145             }
146             ClientConnection o = (ClientConnection) connectedUsers.get(un);
147             if (o.channel.equals(cc.channel)) {
148                 cc.writeObject(new SD_UserAdd(un));
149                 if (o.isAdmin()) {
150                     cc.writeObject(new SD_AdminAdd(un));
151                 }
152             }
153         }
154         cc.writeObject(new SD_UserAdd(null)); // send End of List
155
}
156
157     /**
158      * sends the specified SocketData to the client, to
159      * @param sd the SocketData object to be sent
160      * @param to the user to be sent the data
161      * @return status of the sendTo
162      */

163     synchronized public boolean sendTo(SocketData sd, String JavaDoc to) {
164         ClientConnection o = (ClientConnection) connectedUsers.get(to);
165         if (o != null) {
166             o.writeObject(sd);
167             return true;
168         } else {
169             return false;
170         }
171     }
172
173     /**
174      * broadcases a message to all connected user except from
175      * @param sd the SocketData object to be sent
176      * @param from the user to be avoided when sending data
177      * (usually the user sending the data)
178      */

179     synchronized public void broadcast(SocketData sd, String JavaDoc from) {
180         Enumeration JavaDoc e = connectedUsers.keys();
181         while (e.hasMoreElements()) {
182             String JavaDoc to = (String JavaDoc) e.nextElement();
183             if (!to.equals(from)) {
184                 ClientConnection o = (ClientConnection) connectedUsers.get(to);
185                 o.writeObject(sd);
186             }
187         }
188     }
189
190     /**
191      * broadcases a message to all connected user except from
192      * and is a member of c
193      * @param sd the SocketData object to be sent
194      * @param from the user to be avoided when sending data
195      * (usually the user sending the data)
196      * @param c the channel the user is connected to
197      */

198     synchronized public void broadcast(SocketData sd, String JavaDoc from, String JavaDoc c) {
199         Enumeration JavaDoc e = connectedUsers.keys();
200         while (e.hasMoreElements()) {
201             String JavaDoc to = (String JavaDoc) e.nextElement();
202             if (!to.equals(from)) {
203                 ClientConnection o = (ClientConnection) connectedUsers.get(to);
204                 if (o.channel.equals(c)) {
205                     o.writeObject(sd);
206
207                 }
208             }
209         }
210     }
211
212     /**
213      * sends a SD_UserDel object to all users announcing that un has left
214      * the building
215      * @param cc the user leaving
216      */

217     synchronized public void delete(ClientConnection cc) {
218         broadcast(new SD_UserDel(cc.name), null, cc.channel);
219     }
220
221     /**
222      * Removes a user from the list of connected users and calls delete
223      * @param cc the associated ClientConnection.
224      * @see #delete(ClientConnection cc)
225      */

226     synchronized public void kill(ClientConnection cc) {
227         try {
228             if (cc.name != null && connectedUsers.remove(cc.name) == cc) {
229                 log(cc, cc.name + " disconnected");
230                 updateUserExport();
231                 if (channels.userDel(cc.channel)) {
232                     broadcast(new SD_Channel(true, cc.channel, null), null);
233                     chatLog(cc, false);
234                 }
235                 delete(cc);
236             }
237         } catch (NullPointerException JavaDoc e) {
238             e.printStackTrace();
239             if (cc != null) {
240                 log("null pointer on kill: " + cc.name + " (" + cc.channel + ")");
241             } else {
242                 log("got sent a null cc");
243             }
244         }
245     }
246
247     /**
248      * gets the current date/time of the form MM/DD/YY HH:MM:SS
249      * used for loggin purposes
250      * @return a string specifying the current date/time
251      */

252     public static String JavaDoc getTimestamp() {
253        Calendar JavaDoc rightNow = Calendar.getInstance();
254         return rightNow.get(Calendar.MONTH) + "/" +
255                 rightNow.get(Calendar.DATE) + "/" +
256                 rightNow.get(Calendar.YEAR) + " " +
257                 rightNow.get(Calendar.HOUR_OF_DAY) + ":" +
258                 rightNow.get(Calendar.MINUTE) + ":" +
259                 rightNow.get(Calendar.SECOND);
260     }
261
262     /**
263      * writes to the servers log file preceded by a timestamp
264      * @param s the string to be logged
265      */

266     synchronized private void log(String JavaDoc s) {
267         if (sysLogOut != null) {
268             sysLogOut.println(getTimestamp() + " - " + s);
269             sysLogOut.flush();
270         }
271     }
272
273     /**
274      * a public method used by clients to send log data
275      * @param cc the client sending the log, used to identify
276                     the logger in the log
277      * @param s the string to be logged
278      */

279     public void log(ClientConnection cc, String JavaDoc s) {
280         log(cc.ip + " - " + s);
281     }
282
283     /**
284      * manages the chat log
285      * <br><br><i>do something about channels</i>
286      * @param cc the client attempting to change the log status. used to
287      * determine what channel to work with, if null close all
288      * @param start enables chat logging when true, stops otherwise
289      * @return true on succes
290      */

291     synchronized public boolean chatLog(ClientConnection cc, boolean start) {
292         if (cc == null) {
293             Enumeration JavaDoc e = channelFiles.keys();
294             while (e.hasMoreElements()) {
295                 String JavaDoc name = (String JavaDoc) e.nextElement();
296                 ChatFileItem item = (ChatFileItem)channelFiles.get(name);
297                 closeLog(null, item);
298             }
299             return true;
300         }
301         ChatFileItem currentWriter=(ChatFileItem)channelFiles.get(cc.channel);
302         if (start) {
303             if (currentWriter != null && currentWriter.logging) {
304                 cc.writeObject(new SD_Error("already logging " + cc.channel));
305                 return false;
306             }
307             if (currentWriter == null) {
308                 currentWriter = new ChatFileItem();
309                 channelFiles.put(cc.channel, currentWriter);
310             }
311             String JavaDoc fname = chatLogPath + System.getProperty("file.separator") + "chat-" + cc.channel + ".log";
312             try {
313                 currentWriter.chatOut =
314                         new PrintWriter JavaDoc(new BufferedOutputStream JavaDoc(
315                         new FileOutputStream JavaDoc(new File JavaDoc(fname), true)));
316                 currentWriter.chatOut.println("log started by " +
317                                             (cc!=null?cc.name:"server") +
318                                             " at " + getTimestamp());
319                 currentWriter.chatOut.println("====================================================");
320                 currentWriter.chatOut.flush();
321                 currentWriter.logging = true;
322             } catch (FileNotFoundException JavaDoc e) {
323                 cc.writeObject(new SD_Error("error opening " + fname));
324                 listeningServer.running = false;
325                return false;
326             }
327             return true;
328         }
329
330         return closeLog(cc, currentWriter);
331     }
332
333     /**
334      * utility method to close a specified log
335      * @param cc the client attempting to close
336      * @param currentWriter the ChatFileItem to close
337      * @return true on success, false on failure
338      */

339     private boolean closeLog(ClientConnection cc, ChatFileItem currentWriter) {
340         if (currentWriter == null || currentWriter.chatOut == null) {
341             //cc.writeObject(new SD_Error("not logging"));
342
return false;
343         }
344         currentWriter.chatOut.println("====================================================");
345         currentWriter.chatOut.println("log stopped by " +
346                                             (cc!=null?cc.name:"server") +
347                                             " at " + getTimestamp());
348         currentWriter.chatOut.println("====================================================");
349         currentWriter.chatOut.flush();
350         currentWriter.chatOut.close();
351         currentWriter.chatOut = null;
352         currentWriter.logging = false;
353         if (cc != null) {
354             channelFiles.remove(cc.channel);
355         }
356         return true;
357     }
358
359     /**
360      * logs the specified chat message
361      * @param cc the client chatting
362      * @param message the message to be sent
363      */

364     synchronized public void chatLog(ClientConnection cc, String JavaDoc message) {
365         ChatFileItem currentWriter = (ChatFileItem)channelFiles.get(cc.channel);
366         if (currentWriter != null && currentWriter.logging &&
367                         currentWriter.chatOut != null) {
368             currentWriter.chatOut.println(cc.name + ": " + message);
369             currentWriter.chatOut.flush();
370         }
371     }
372
373     /**
374      * allows for the user listing (newline separated) to be exported
375      * to the filesystem, should be called whenever users are added or
376      * removed or renamed
377      * @return true on succes, false otherwise
378      */

379     synchronized static public boolean updateUserExport() {
380         if (userExportFile == null) {
381             return false;
382         }
383         try {
384             PrintWriter JavaDoc pw = new PrintWriter JavaDoc(new BufferedOutputStream JavaDoc(
385                     new FileOutputStream JavaDoc(new File JavaDoc(userExportFile), false)));
386             Enumeration JavaDoc e = connectedUsers.keys();
387             int i;
388             for (i = 0; e.hasMoreElements(); i++) {
389                 String JavaDoc usr = (String JavaDoc) e.nextElement();
390                 ClientConnection o = (ClientConnection)connectedUsers.get(usr);
391                 pw.println(usr + " (" + o.channel + ")");
392             }
393
394             if (i == 0) {
395                 pw.println("(no connected users)");
396             }
397
398             pw.close();
399         } catch (FileNotFoundException JavaDoc e) {
400             System.err.println("unable to access userExportFile: " +
401                                                         userExportFile);
402             userExportFile = null;
403         }
404         return true;
405     }
406
407     /**
408      * creates a new channel, assuming the channel can be created
409      * @param name the name of the channel to be craeted
410      * @param pass the password for the channel
411      * @param cc the client requesting the channel
412      * @return true if created
413      */

414     synchronized public String JavaDoc newChannel(String JavaDoc name, String JavaDoc pass, ClientConnection cc) {
415         String JavaDoc ret;
416         if ((ret = channels.addUserChannel(name, pass, cc)) != null) {
417             return ret;
418         }
419         broadcast(new SD_Channel(true, name, (pass == null ? null : "")), null);
420         return null;
421     }
422
423     /**
424      * checks to see if the specified channel exists
425      * @param name the name of the channel
426      * @return true if name exists exists
427      */

428     synchronized public boolean channelExists(String JavaDoc name) {
429         return channels.channelExists(name);
430     }
431
432     /**
433      * the main loop to recieving incoming clients listens on a secure
434      * server socket creates a shutdown hook to catch a server kill
435      * and exit gracefully
436      */

437     public void run() {
438         Runtime.getRuntime().addShutdownHook(new ShutdownThread(this));
439         try {
440             SSLServerSocketFactory factory =
441                 (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
442             SSLServerSocket sslIncoming =
443                 (SSLServerSocket) factory.createServerSocket (PORT);
444             String JavaDoc [] enabledCipher = sslIncoming.getSupportedCipherSuites ();
445             sslIncoming.setEnabledCipherSuites (enabledCipher);
446
447             log("Server Started");
448             while(running) {
449                 SSLSocket s = (SSLSocket)sslIncoming.accept();
450                 newUser(s);
451             }
452         } catch (IOException JavaDoc e) { System.out.println("Error: " + e); }
453         log("Server Stopped");
454     }
455
456
457     /**
458      * a useless constructor
459      */

460     LlamaChatServer() {
461         running = true;
462     }
463
464     /**
465      * main method for class; initializes lists and system log file
466      * @param args the command line arguments
467      */

468     public static void main(String JavaDoc args[]) {
469         listeningServer = new LlamaChatServer();
470         //listeningServer.running = true;
471

472         connectingUsers = new LinkedList JavaDoc();
473         connectedUsers = new Hashtable JavaDoc();
474         channels = new ChannelManager();
475         channelFiles = new Hashtable JavaDoc();
476
477
478         try {
479             DefaultHandler JavaDoc handler = new ConfigParser(listeningServer);
480             SAXParserFactory JavaDoc factory = SAXParserFactory.newInstance();
481             SAXParser JavaDoc saxParser = factory.newSAXParser();
482             saxParser.parse(new File JavaDoc(serverConfigFile), handler);
483         } catch (Throwable JavaDoc t) {
484             System.err.println("error parsing " + serverConfigFile);
485             running = false;
486         }
487
488         try {
489             sysLogOut = new PrintWriter JavaDoc(new BufferedOutputStream JavaDoc(
490                         new FileOutputStream JavaDoc(new File JavaDoc(sysLogFile), true)));
491         } catch (FileNotFoundException JavaDoc e) {
492             System.err.println(sysLogFile + " not found.");
493             running = false;
494         }
495
496         updateUserExport();
497
498         listeningServer.start();
499         try {
500             listeningServer.join();
501         } catch (InterruptedException JavaDoc e) { }
502     }
503
504     /**
505      * A class to enable gracefull shutdown when unexpectadly killed
506      */

507     public class ShutdownThread extends Thread JavaDoc {
508         private LlamaChatServer lcs;
509         ShutdownThread(LlamaChatServer l) {
510             lcs = l;
511         }
512         public void run() {
513             log("Server Stopped");
514             sysLogOut.close();
515             chatLog(null, false);
516         }
517     }
518
519     /**
520      * a class to hold logging information on a Hashtable
521      */

522     private class ChatFileItem {
523         public boolean logging;
524         public PrintWriter JavaDoc chatOut;
525
526         ChatFileItem() {
527             logging = false;
528             chatOut = null;
529         }
530     }
531 }
532
Popular Tags