import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; /** * Jircs: Java IRC Server. This is a simplistic IRC server. It's not designed * for production environments; I mainly wrote it to allow me to test out an IRC * bot I'm writing (http://jzbot.googlecode.com) when I'm not connected to the * internet. Every other dang IRC server takes a minute or so to try and look up * my hostname before realizing that I'm not even connected to the internet. * Jircs specifically doesn't do that. * * @author Alexander Boyd * */ public class Connection implements Runnable { public static class Channel { private ArrayList channelMembers = new ArrayList(); private String topic; protected String name; public void sendNot(Connection not, String toSend) { synchronized (mutex) { for (Connection con : channelMembers) { if (con != not) con.send(toSend); } } } public void send(String toSend) { sendNot(null, toSend); } public void memberQuit(String nick) { } } public static final Object mutex = new Object(); private Socket socket; private String username; private String hostname; private String nick; private String description; public static Map connectionMap = new HashMap(); public static Map channelMap = new HashMap(); private static String globalServerName; public Connection(Socket socket) { this.socket = socket; } public String getRepresentation() { return nick + "!" + username + "@" + hostname; } /** * @param args */ public static void main(String[] args) throws Throwable { if (args.length == 0) { System.out.println("Usage: java jw.jircs.Connection "); return; } globalServerName = args[0]; ServerSocket ss = new ServerSocket(6667); while (true) { Socket s = ss.accept(); Connection jircs = new Connection(s); Thread thread = new Thread(jircs); thread.start(); } } public enum Command { NICK(1, 1) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { if (con.nick == null) doFirstTimeNick(con, arguments[0]); else doSelfSwitchNick(con, arguments[0]); } private void doSelfSwitchNick(Connection con, String nick) { synchronized (mutex) { String oldNick = con.nick; con.nick = filterAllowedNick(nick); connectionMap.remove(oldNick); connectionMap.put(con.nick, con); con.send(":" + oldNick + "!" + con.username + "@" + con.hostname + " NICK :" + con.nick); /* * Now we need to notify all channels that we are on */ for (Channel c : channelMap.values()) { if (c.channelMembers.contains(con)) c .sendNot(con, ":" + oldNick + "!" + con.username + "@" + con.hostname + " NICK :" + con.nick); } } } private void doFirstTimeNick(Connection con, String nick) throws InterruptedException { con.nick = filterAllowedNick(nick); synchronized (mutex) { connectionMap.put(con.nick, con); } /* * Now we send the user a welcome message and everything. */ } }, USER(1, 4) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { if (con.username != null) { con.send("NOTICE AUTH :You can't change your user " + "information after you've logged in right now."); return; } con.username = arguments[0]; String forDescription = arguments.length > 3 ? arguments[3] : "(no description)"; con.description = forDescription; /* * Now we'll send the user their initial information. */ con.sendGlobal("001 " + con.nick + " :Welcome to " + globalServerName + ", a Jircs-powered IRC network."); con.sendGlobal("004 " + con.nick + " " + globalServerName + " Jircs"); con.sendGlobal("375 " + con.nick + " :- " + globalServerName + " Message of the Day -"); con.sendGlobal("372 " + con.nick + " :- Hello. Welcome to " + globalServerName + ", a Jircs-powered IRC network."); con .sendGlobal("372 " + con.nick + " :- See http://code.google.com/p/jwutils/wiki/Jircs " + "for more info on Jircs."); con.sendGlobal("376 " + con.nick + " :End of /MOTD command."); } }, PING(1, 1) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { con.send(":" + globalServerName + " PONG " + globalServerName + " :" + arguments[0]); } }, JOIN(1, 2) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { if (arguments.length == 2) { con.sendSelfNotice("This server does not support " + "channel keys at " + "this time. JOIN will act as if you " + "hadn't specified any keys."); } String[] channelNames = arguments[0].split(","); for (String channelName : channelNames) { if (!channelName.startsWith("#")) { con.sendSelfNotice("This server only allows " + "channel names that " + "start with a # sign."); return; } if (channelName.contains(" ")) { con.sendSelfNotice("This server does not allow spaces " + "in channel names."); return; } } for (String channelName : channelNames) { doJoin(con, channelName); } } public void doJoin(Connection con, String channelName) { if (!channelName.startsWith("#")) { con .sendSelfNotice("This server only allows channel names that " + "start with a # sign."); return; } if (channelName.contains(" ")) { con .sendSelfNotice("This server does not allow spaces in channel names."); } synchronized (mutex) { Channel channel = channelMap.get(channelName); boolean added = false; if (channel == null) { added = true; channel = new Channel(); channel.name = channelName; channelMap.put(channelName, channel); } if (channel.channelMembers.contains(con)) { con.sendSelfNotice("You're already a member of " + channelName); return; } channel.channelMembers.add(con); channel.send(":" + con.getRepresentation() + " JOIN " + channelName); if (added) con.sendGlobal("MODE " + channelName + " +nt"); // This is commented out because channel.send takes care of // this for us // con.send(":" + con.getRepresentation() + " JOIN " // + channelName); if (channel.topic != null) con.sendGlobal("332 " + con.nick + " " + channel.name + " :" + channel.topic); else con.sendGlobal("331 " + con.nick + " " + channel.name + " :No topic is set"); for (Connection channelMember : channel.channelMembers) {// 353,366 con.sendGlobal("353 " + con.nick + " = " + channelName + " :" + channelMember.nick); } con.sendGlobal("366 " + con.nick + " " + channelName + " :End of /NAMES list"); } } }, WHO(0, 2) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { if (arguments.length > 1) con .sendSelfNotice("Filtering by operator only using the WHO " + "command isn't yet supported. WHO will act " + "as if \"o\" has not been specified."); String person = ""; if (arguments.length > 0) person = arguments[0]; synchronized (mutex) { Channel channel = channelMap.get(person); if (channel != null) { for (Connection channelMember : channel.channelMembers) { con.sendGlobal("352 " + con.nick + " " + person + " " + channelMember.username + " " + channelMember.hostname + " " + globalServerName + " " + channelMember.nick + " H :0 " + channelMember.description); } } else { con .sendSelfNotice("WHO with something other than a channel " + "as arguments is not supported right now. " + "WHO will display an empty list of people."); } } con.send("315 " + con.nick + " " + person + " :End of /WHO list."); } }, USERHOST(1, 5) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { ArrayList replies = new ArrayList(); for (String s : arguments) { Connection user = connectionMap.get(s); if (user != null) replies.add(user.nick + "=+" + user.username + "@" + user.hostname); } con.sendGlobal("302 " + con.nick + " :" + delimited(replies.toArray(new String[0]), " ")); } }, MODE(0, 2) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { if (arguments.length == 1) { if (arguments[0].startsWith("#")) { con.sendGlobal("324 " + con.nick + " " + arguments[0] + " +nt"); } else { con .sendSelfNotice("User mode querying not supported yet."); } } else if (arguments.length == 2 && (arguments[1].equals("+b") || arguments[1] .equals("+e"))) { if (arguments[0].startsWith("#")) {// 368,349 if (arguments[1].equals("+b")) { con.sendGlobal("368 " + con.nick + " " + arguments[0] + " :End of channel ban list"); } else { con.sendGlobal("349 " + con.nick + " " + arguments[0] + " :End of channel exception list"); } } else { con .sendSelfNotice("User mode setting not supported yet for +b or +e."); } } else { con.sendSelfNotice("Specific modes not supported yet."); } } }, PART(1, 2) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { String[] channels = arguments[0].split(","); for (String channelName : channels) { synchronized (mutex) { Channel channel = channelMap.get(channelName); if (channelName == null) con .sendSelfNotice("You're not a member of the channel " + channelName + ", so you can't part it."); else { channel.send(":" + con.getRepresentation() + " PART " + channelName); channel.channelMembers.remove(con); if (channel.channelMembers.size() == 0) channelMap.remove(channelName); } } } } }, QUIT(1, 1) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { con.sendQuit("Quit: " + arguments[0]); } }, PRIVMSG(2, 2) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { String[] recipients = arguments[0].split(","); String message = arguments[1]; for (String recipient : recipients) { if (recipient.startsWith("#")) { Channel channel = channelMap.get(recipient); if (channel == null) con .sendSelfNotice("No such channel, so can't send " + "a message to it: " + recipient); else if (!channel.channelMembers.contains(con)) con.sendSelfNotice("You can't send messages to " + "channels you're not at."); else channel.sendNot(con, ":" + con.getRepresentation() + " PRIVMSG " + recipient + " :" + message); } else { Connection recipientConnection = connectionMap .get(recipient); if (recipientConnection == null) con.sendSelfNotice("The user " + recipient + " is not online."); else recipientConnection.send(":" + con.getRepresentation() + " PRIVMSG " + recipient + " :" + message); } } } }, TOPIC(1, 2) { @Override public void run(Connection con, String prefix, String[] arguments) throws Exception { Channel channel = channelMap.get(arguments[0]); if (channel == null) { con.sendSelfNotice("No such channel for topic viewing: " + arguments[0]); return; } if (arguments.length == 1) { /* * The user wants to see the channel topic. */ if (channel.topic != null) con.sendGlobal("332 " + con.nick + " " + channel.name + " :" + channel.topic); else con.sendGlobal("331 " + con.nick + " " + channel.name + " :No topic is set"); } else { /* * The user wants to set the channel topic. */ channel.topic = arguments[1]; channel.sendNot(con, ":" + con.getRepresentation() + " TOPIC " + channel.name + " :" + channel.topic); } } }; private int minArgumentCount; private int maxArgumentCount; private Command(int min, int max) { minArgumentCount = min; maxArgumentCount = max; } public int getMin() { return minArgumentCount; } public int getMax() { return maxArgumentCount; } public abstract void run(Connection con, String prefix, String[] arguments) throws Exception; } public static String delimited(String[] items, String delimiter) { StringBuffer response = new StringBuffer(); boolean first = true; for (String s : items) { if (first) first = false; else response.append(delimiter); response.append(s); } return response.toString(); } protected void sendQuit(String quitMessage) { synchronized (mutex) { for (String channelName : new ArrayList(channelMap.keySet())) { Channel channel = channelMap.get(channelName); channel.channelMembers.remove(this); channel.send(":" + getRepresentation() + " QUIT :" + quitMessage); if (channel.channelMembers.size() == 0) channelMap.remove(channel.name); } } } @Override public void run() { try { doServer(); } catch (Exception e) { try { socket.close(); } catch (Exception e2) { } e.printStackTrace(); } finally { if (nick != null && connectionMap.get(nick) == this) { sendQuit("Client disconnected"); } } } protected void sendGlobal(String string) { send(":" + globalServerName + " " + string); } private LinkedBlockingQueue outQueue = new LinkedBlockingQueue( 1000); private Thread outThread = new Thread() { public void run() { try { OutputStream out = socket.getOutputStream(); while (true) { String s = outQueue.take(); s = s.replace("\n", "").replace("\r", ""); s = s + "\r\n"; out.write(s.getBytes()); out.flush(); } } catch (Exception e) { System.out.println("Outqueue died"); outQueue.clear(); outQueue = null; e.printStackTrace(); try { socket.close(); } catch (Exception e2) { e2.printStackTrace(); } } } }; private void doServer() throws Exception { InetSocketAddress address = (InetSocketAddress) socket .getRemoteSocketAddress(); hostname = address.getAddress().getHostAddress(); System.out.println("Connection from host " + hostname); outThread.start(); InputStream socketIn = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader( socketIn)); String line; while ((line = reader.readLine()) != null) { processLine(line); } } private void processLine(String line) throws Exception { System.out.println("Processing line from " + nick + ": " + line); String prefix = ""; if (line.startsWith(":")) { String[] tokens = line.split(" ", 2); prefix = tokens[0]; line = (tokens.length > 1 ? tokens[1] : ""); } String[] tokens1 = line.split(" ", 2); String command = tokens1[0]; line = tokens1.length > 1 ? tokens1[1] : ""; String[] tokens2 = line.split("(^| )\\:", 2); String trailing = null; line = tokens2[0]; if (tokens2.length > 1) trailing = tokens2[1]; ArrayList argumentList = new ArrayList(); if (!line.equals("")) argumentList.addAll(Arrays.asList(line.split(" "))); if (trailing != null) argumentList.add(trailing); String[] arguments = argumentList.toArray(new String[0]); /* * Now we actually process the command. */ if (command.matches("[0-9][0-9][0-9]")) command = "n" + command; Command commandObject = null; try { Command.valueOf(command.toLowerCase()); } catch (Exception e) { } if (commandObject == null) { try { commandObject = Command.valueOf(command.toUpperCase()); } catch (Exception e) { } } if (commandObject == null) { sendSelfNotice("That command (" + command + ") isnt a supported command at this server."); return; } if (arguments.length < commandObject.getMin() || arguments.length > commandObject.getMax()) { sendSelfNotice("Invalid number of arguments for this" + " command, expected not more than " + commandObject.getMax() + " and not less than " + commandObject.getMin() + " but got " + arguments.length + " arguments"); return; } commandObject.run(this, prefix, arguments); } /** * Sends a notice from the server to the user represented by this * connection. * * @param string * The text to send as a notice */ private void sendSelfNotice(String string) { send(":" + globalServerName + " NOTICE " + nick + " :" + string); } public static String filterAllowedNick(String theNick) { return theNick.replace(":", "").replace(" ", "").replace("!", "") .replace("@", "").replace("#", ""); } private String[] padSplit(String line, String regex, int max) { String[] split = line.split(regex); String[] output = new String[max]; for (int i = 0; i < output.length; i++) { output[i] = ""; } for (int i = 0; i < split.length; i++) { output[i] = split[i]; } return output; } public void send(String s) { Queue testQueue = outQueue; if (testQueue != null) { System.out.println("Sending line to " + nick + ": " + s); testQueue.add(s); } } }