package com.external.stomp; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.util.*; /** * Implements a Stomp server. This is a tiny embeddable server that * can be used as an inter-net or intra-VM server to handle Stomp * requests. * * For intra-VM requests, use getClient() to get an instance of a Stomp * client. * * Example usage: * <pre> * Server s = new Server( 61656 ); // To start it * s.listen( 12345 ); // To listen on another port * // Note that 's' is now listening on TWO ports * s.close( 61656 ); // To close a port * s.close( 12345 ); // Now no ports are open. * // This is effectively the same as: * i = new Server(); // A server with no network (intra-VM) * Stomp c = s.getClient(); // Creates and returns a client * // that is connected to the server directly via VM method calls -- * // there is no network communication between this client and the * // server. * s.stop(); // To stop the server * i.stop(); * </pre> * * FIXME * Queues are not implemented. Therefore, this server operates as an IRC, * rather than a Jabber, messaging system. That is, all messages arriving * before a subscription request are lost to that client. When Queues are * implemented, there will be an option to set persistence on the messages. * * Would it be good if -- given a session ID -- clients could * reconnect and complete transactions? * * (c)2005 Sean Russell */ public class Server { private Queue _message_queue; private Map _transactions; private Map _listeners; private ConnectionListener _connection_listener; private Authenticator _authenticator = new AllowAllAuthenticator(); /** * Instantiates an intra-VM server. This will open no ports, and * can not be connected to except by intra-VM threads. A port can * be opened on this server by using the listen() method. * * @see listen() */ public Server() { _message_queue = new FileQueue(); _transactions = new HashMap(); _listeners = new HashMap(); } /** * Instantiates an inter-network server listening on the supplied * port. Additional ports can be listened on by using the listen() * method. * * @see listen() * @param port This port will be opened and will listen for client * connections. If the port value is less than 0, the default port * of 61626 will be used. */ public Server( int port ) throws IOException { this( port, null ); } /** * Instantiates an inter-network server listening on the supplied * port. Additional ports can be listened on by using the listen() * method. * * @see listen() * @param port This port will be opened and will listen for client * connections. If the port value is less than 0, the default port * of 61626 will be used. * @param auth A class responsible for authenticating connections. */ public Server( int port, Authenticator auth ) throws IOException { this(); if (port < 0) port = 61626; if (auth != null) _authenticator = auth; listen(port); } /** * Opens a port for internet connections. A single server can listen * on multiple ports, so calling this method multiple times will open * multiple ports. * * @param port This port will be opened and will listen for client * connections. If the port value is less than 0, an exception is * thrown. */ public void listen( int port ) throws IOException { _connection_listener = new ConnectionListener( port, this ); _connection_listener.start(); } /** * Called by a SocketHandler to notify the server that a client * has disconnected. Is not, and should not, be called from anywhere * else. */ protected void disconnect( SocketHandler s ) { _connection_listener.disconnect( s ); } /** * This class is necessary because Java is RETARDED. Specifically, * it lacks closures. * * Listenens on a port and accepts client connections. For each * connection, spawns a SocketHandler for the connection. When * shut down, stops receiving connections and shuts down all existing * client connections. */ private class ConnectionListener extends Thread { private int _port; private Server _server; private ServerSocket _serve_sock; private List _handlers = new ArrayList(); protected ConnectionListener( int port, Server server ) { _port = port; _server = server; } public void run() { Socket sock = null; try { _serve_sock = new ServerSocket( _port ); while (!isInterrupted()) { sock = _serve_sock.accept(); try { Thread handler = new SocketHandler( sock, _server ); handler.start(); _handlers.add( handler ); } catch (IOException e) { e.printStackTrace( System.err ); } } } catch (SocketException e) { // This gets thrown when the accept() is interrupted } catch (IOException e) { e.printStackTrace( System.err ); } catch (Exception e) { e.printStackTrace( System.err ); } for (Iterator i=_handlers.iterator(); i.hasNext(); ) { try { Thread t = (Thread)i.next(); t.interrupt(); Thread.yield(); } catch (Exception e) { } } } /** * Shut down operations. */ protected void shutdown() { this.interrupt(); try { _serve_sock.close(); } catch (Exception e) {} } /** * Called by the server to notify this object that a SocketHandler * has disconnected itself. */ protected void disconnect( SocketHandler h ) { _handlers.remove(h); } } /** * Shuts down the server, closing all connections. */ public void stop() { // The _connection_listener will be null if this is not a network // socket server. if (_connection_listener != null) { _connection_listener.shutdown(); } close( -1 ); Thread.yield(); } /** * Sets the queuing mechanism used for all further messages. Any * existing undelivered messages will <em>not</em> use this queue. * FIXME Not implemented * * @param queue */ public void setQueue( Queue q ) { _message_queue = q; } /** * Closes a port. All connections on this port will be closed. * * @param port The port to close. A value of < -1 closes all ports */ public void close( int port ) { for (Iterator i = _listeners.keySet().iterator(); i.hasNext(); ) { Object k = i.next(); Object s = _listeners.get(k); if (s instanceof SocketHandler) { SocketHandler sh = (SocketHandler)s; if (port == -1 || sh.isPort( port )) { sh.interrupt(); sh.close(); _transactions.remove( s ); _listeners.remove( k ); } } } } // FIXME: Need to enforce CONNECT; right now, doesn't require a connect. // FIXME: Add login handling feature /** * Manages client connections. There is one SocketHandler per client. * This class is responsible for relaying communications between the * server and the client for which it is responsible. */ protected class SocketHandler extends Receiver implements Listener, Authenticatable { private InputStream _input; private OutputStream _output; private Socket _socket; private Server _server; private Object _client_token; private boolean _authenticated = false; /** * Sets up a client communication on a given socket. */ public SocketHandler( Socket sock, Server s ) throws IOException { super(); _input = sock.getInputStream(); _output = sock.getOutputStream(); _socket = sock; _server = s; setup( this, _input ); } public Object token() { return _client_token; } public boolean isClosed() { return _socket.isClosed(); } /** * Tests whether the supplied port is the port this handler is * communicating with the client over. * * @param port the port number to test. * @return true iff the supplied port matches the open port of this * handler. */ protected boolean isPort( int port ) { return _socket.getPort() == port; } /** * Close the connection with the client. */ protected void close() { try { _socket.shutdownInput(); _input.close(); } catch (IOException e) { /* Who cares? */ } try { _socket.shutdownOutput(); _output.close(); } catch (IOException e) { /* Who cares? */ } try { _socket.close(); } catch (IOException e) { /* Who cares? */ } } public void disconnect() { close(); } /** * Gets called when messages come in from the client, and relays the * message to the server. This method handles and consumes CONNECT, * DISCONNECT, and ERROR messages. It is also responsible for sending * RECEIPTs back to the client. */ public void receive( Command c, Map h, String b ) { if (c == Command.CONNECT) { String login = (String)h.get( "login" ); String passcode = (String)h.get( "passcode" ); try { _client_token = Server.this._authenticator.connect( login, passcode ); HashMap headers = new HashMap(); headers.put( "session", String.valueOf( this.hashCode() ) ); transmit( Command.CONNECTED, headers, null ); _authenticated = true; } catch (javax.security.auth.login.LoginException e) { transmit( Command.ERROR, null, "Login failed: " + e.getMessage()); } } else { if (!_authenticated) { transmit( Command.ERROR, null, "Not CONNECTed, or not authorized" ); return; } if (c == Command.DISCONNECT) { if (h != null) { String receipt = (String)h.get("receipt"); if (receipt != null) { HashMap headers = new HashMap(); headers.put( "receipt-id", receipt ); receive( Command.RECEIPT, headers, null ); } } _server.disconnect( this ); this.interrupt(); Thread.yield(); close(); } else if (c == Command.ERROR) { // Then there was an error in the client message. Pass it back. error( h, b ); } else { _server.receive( c, h, b, this ); } } } /** * Called by the server; sends a message to this client. */ public void message( Map headers, String body ) { transmit( Command.MESSAGE, headers, body ); } /** * Called by the server; sends a receipt to this client. */ public void receipt( Map headers ) { transmit( Command.RECEIPT, headers, null ); } /** * Called by the server. Sends an error to the client. */ public void error( Map headers, String message ) { transmit( Command.ERROR, headers, message ); } /** * Used by message(), receipt(), and error() to deliver the message to the * client. */ private void transmit( Command c, Map h, String b ) { try { Transmitter.transmit( c, h, b, _output ); } catch (Exception e) { this.interrupt(); Thread.yield(); close(); } } } private String mapToStr( Map m ) { StringBuffer b = new StringBuffer("[ "); for (Iterator keys = m.keySet().iterator(); keys.hasNext(); ) { String k = keys.next().toString(); b.append( k+" => "+m.get(k)+", " ); } b.append( "]" ); return b.toString(); } /** * Incoming mesages from clients come here, and are delivered to listeners, * both intra-VM and network. * * @param c the command * @param h the headers * @param b the message * @param y the thing that received the message and passed it to us */ protected void receive( Command c, Map h, String b, Authenticatable y ) { long id = (int)(Math.random() * 10000); try { // Convert to MESSAGE and distribute if (c == Command.COMMIT) { synchronized (_transactions) { List trans = (List)_transactions.remove(y); trans = new ArrayList( trans ); for (Iterator i=trans.iterator(); i.hasNext(); ) { Message m = (Message)i.next(); try { receive( m.command(), m.headers(), m.body(), y ); } catch (Exception e) { // Don't allow listener code to break us } } } } else if (c == Command.ABORT) { synchronized (_transactions) { _transactions.remove(y); } } else if (_transactions.get( y ) != null) { synchronized (_transactions) { ((List)_transactions.get( y )).add( new Message( c, h, b ) ); } } else { if (h == null) h = new HashMap(); String destination = (String)h.get("destination"); if (c == Command.SEND) { if (y instanceof IntraVMClient || _authenticator.authorizeSend( y.token(), destination )) { synchronized( _listeners ) { List l = (List)_listeners.get( destination ); if (l != null) { l = new ArrayList(l); for (Iterator i = l.iterator(); i.hasNext(); ) { Listener sh = (Listener)i.next(); try { sh.message( h, b ); } catch (Exception e) { // Don't allow listener code to break us } } } } } else { Map error_headers = new HashMap(); error_headers.put( "message:", "authorization refused"); error_headers.put( "type:", "send"); error_headers.put( "channel:", destination); y.error( error_headers, "The message:\n-----\n"+b+ "\n-----\nAuthentication token refused for this channel"); } } else if (c == Command.SUBSCRIBE) { if (y instanceof IntraVMClient || _authenticator.authorizeSubscribe( y.token(), destination )) { synchronized (_listeners ) { List l = (List)_listeners.get( destination ); if (l == null) { l = new ArrayList(); _listeners.put( destination, l ); } if (!l.contains(y)) l.add( y ); } } else { Map error_headers = new HashMap(); error_headers.put( "message:", "authorization refused"); error_headers.put( "type:", "subscription"); error_headers.put( "channel:", destination); y.error( error_headers, "The message:\n-----\n"+b+ "\n-----\nAuthentication token refused for this channel"); } } else if (c == Command.UNSUBSCRIBE) { synchronized (_listeners) { List l = (List)_listeners.get( destination ); if (l != null) l.remove( y ); } } else if (c == Command.BEGIN) { synchronized (_transactions) { List trans = new ArrayList(); _transactions.put( y, trans ); } } else if (c == Command.DISCONNECT) { synchronized (_listeners) { for (Iterator i=_listeners.values().iterator(); i.hasNext(); ) { List l = (List)i.next(); l.remove( y ); } } } } if (h != null) { String receipt = (String)h.get("receipt"); if (receipt != null) { HashMap headers = new HashMap(); headers.put( "receipt-id", receipt ); y.receive( Command.RECEIPT, headers, null ); } } } catch (Exception e) { // Don't allow listener code to break us } } /** * Returns a Stomp client for intra-VM communications with the server. * This client communicates directly with the server via method() calls, * bypassing the network. */ public Stomp getClient() { return new IntraVMClient( this ); } /** * Gozirra is probably not the best choice for a stand-alone server. If * you are tempted to use it as such, you might want to look at ActiveMQ, * which is a feature-rich MOM solution. Gozirra is intended primarily * to be an ultra-light embedded messaging library. * * If you still want to run Gozirra as a stand-alone server, then you'll * need to know about how to call it. The main() method takes a single * argument: a port to run on. It will run until you ^C it. */ public static final void main( String[] args ) { if (args.length != 1) { System.err.println("A single argument -- a port -- is required"); System.exit(1); } int port = Integer.valueOf( args[0] ).intValue(); System.out.println(Version.VERSION); try { new Server( port ); } catch (Exception e) { System.err.println("Failed to start server"); e.printStackTrace( System.err ); } } }