diff --git a/docs/presents/design.txt b/docs/presents/design.txt index 1f5d78fbb..34f8357b1 100644 --- a/docs/presents/design.txt +++ b/docs/presents/design.txt @@ -36,3 +36,19 @@ the outgoing socket Informs exo-client about connection state changes; provides interface to connection + authentication (logon) and disconnection (logoff); provides access to omgr and client dobj + +* Server components + +** Connection Manager +Listens on accepting socket; creates and manages connection objects; +informs connection observer of state changes; handles all network traffic +on own thread; + +** Auth Manager +Processes auth requests on own thread; uses pluggable Authenticator to +perform actual authentication; + +** Client Manager +Registers with connection manager; manages authentication; maps +connections to existing client objects or creates new client objects for +newly connecting clients; diff --git a/docs/presents/notes.txt b/docs/presents/notes.txt index a3ddfbc09..6cab68ae7 100644 --- a/docs/presents/notes.txt +++ b/docs/presents/notes.txt @@ -1,5 +1,11 @@ Cher Mk3 Notes -*- outline -*- +* Check into "connection closed by peer" thread exiting on client + +* TypedObjectFactory +Maybe modify so that types are assigned automatically even if everything +has to be registered in a single place, since it pretty much does anyway. + * Server components: + Connection manager + Client manager diff --git a/src/java/com/threerings/presents/Log.java b/src/java/com/threerings/presents/Log.java index dbc43ec4e..3fdc2c8b4 100644 --- a/src/java/com/threerings/presents/Log.java +++ b/src/java/com/threerings/presents/Log.java @@ -1,5 +1,5 @@ // -// $Id: Log.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ +// $Id: Log.java,v 1.2 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher; @@ -10,7 +10,7 @@ package com.samskivert.cocktail.cher; public class Log { public static com.samskivert.util.Log log = - new com.samskivert.util.Log("robodj"); + new com.samskivert.util.Log("cher"); /** Convenience function. */ public static void debug (String message) diff --git a/src/java/com/threerings/presents/client/Client.java b/src/java/com/threerings/presents/client/Client.java index cc5d7f8af..050cde0a0 100644 --- a/src/java/com/threerings/presents/client/Client.java +++ b/src/java/com/threerings/presents/client/Client.java @@ -1,5 +1,5 @@ // -// $Id: Client.java,v 1.2 2001/05/23 04:03:40 mdb Exp $ +// $Id: Client.java,v 1.3 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.client; @@ -8,6 +8,7 @@ import java.util.List; import com.samskivert.cocktail.cher.Log; import com.samskivert.cocktail.cher.net.Credentials; +import com.samskivert.cocktail.cher.net.Registry; /** * Through the client object, a connection to the system is established @@ -194,4 +195,9 @@ public class Client static final int CLIENT_CONNECTION_FAILED = 2; static final int CLIENT_WILL_LOGOFF = 3; static final int CLIENT_DID_LOGOFF = 4; + + // register our shared objects + static { + Registry.registerTypedObjects(); + } } diff --git a/src/java/com/threerings/presents/client/Communicator.java b/src/java/com/threerings/presents/client/Communicator.java index cd737ef95..6703aee47 100644 --- a/src/java/com/threerings/presents/client/Communicator.java +++ b/src/java/com/threerings/presents/client/Communicator.java @@ -1,5 +1,5 @@ // -// $Id: Communicator.java,v 1.3 2001/05/23 04:03:40 mdb Exp $ +// $Id: Communicator.java,v 1.4 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.client; @@ -139,6 +139,8 @@ class Communicator */ protected synchronized void logonSucceeded (AuthResponseData data) { + Log.info("Logon succeeded: " + data); + // extract bootstrap information // create a new writer thread and start it up @@ -147,6 +149,9 @@ class Communicator } _writer = new Writer(); _writer.start(); + + // let the client know that logon succeeded + _client.notifyObservers(Client.CLIENT_DID_LOGON, null); } /** @@ -161,6 +166,8 @@ class Communicator return; } + Log.info("Connection failed: " + ioe); + // let the client know that things went south _client.notifyObservers(Client.CLIENT_CONNECTION_FAILED, ioe); @@ -212,10 +219,15 @@ class Communicator protected DownstreamMessage receiveMessage () throws IOException { - // read in the next message frame - _fin.clearAndReadFrame(_in); - // then use the typed object factory to read and decode the proper - // downstream message instance + // read in the next message frame (readFrame() can return false + // meaning it only read part of the frame from the network, in + // which case we simply call it again because we can't do anything + // until it has a whole frame; it will throw an exception if it + // hits EOF or if something goes awry) + while (!_fin.readFrame(_in)); + + // then use the typed object factory to read and decode the + // proper downstream message instance return (DownstreamMessage)TypedObjectFactory.readFrom(_din); } @@ -225,6 +237,7 @@ class Communicator */ protected void processMessage (DownstreamMessage msg) { + Log.info("Process msg: " + msg); } /** @@ -247,6 +260,8 @@ class Communicator logon(); } catch (Exception e) { + Log.info("Logon failed: " + e); + Log.logStackTrace(e); // let the observers know that we've failed _client.notifyObservers(Client.CLIENT_FAILED_TO_LOGON, e); // and terminate our communicator thread diff --git a/src/java/com/threerings/presents/dobj/DObject.java b/src/java/com/threerings/presents/dobj/DObject.java index 798792e87..a0e3a3005 100644 --- a/src/java/com/threerings/presents/dobj/DObject.java +++ b/src/java/com/threerings/presents/dobj/DObject.java @@ -1,5 +1,5 @@ // -// $Id: DObject.java,v 1.2 2001/05/23 04:03:40 mdb Exp $ +// $Id: DObject.java,v 1.3 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.dobj; @@ -7,25 +7,17 @@ import java.io.IOException; import java.io.DataInputStream; import java.io.DataOutputStream; -import com.samskivert.cocktail.cher.io.TypedObject; -import com.samskivert.cocktail.cher.io.TypedObjectFactory; - -public class DObject implements TypedObject +public class DObject { - public static short TYPE = 400; - - public short getType () - { - return TYPE; - } - public void writeTo (DataOutputStream out) throws IOException { + // nothing doing! } public void readFrom (DataInputStream in) throws IOException { + // nothing doing! } } diff --git a/src/java/com/threerings/presents/dobj/io/DObjectFactory.java b/src/java/com/threerings/presents/dobj/io/DObjectFactory.java new file mode 100644 index 000000000..6371cf9ac --- /dev/null +++ b/src/java/com/threerings/presents/dobj/io/DObjectFactory.java @@ -0,0 +1,42 @@ +// +// $Id: DObjectFactory.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.dobj; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import com.samskivert.cocktail.cher.io.ObjectStreamException; + +/** + * The distributed object factory is responsible for marshalling and + * unmarshalling distributed objects to and from streams so that they can + * be communicated between the client and server. + */ +public class DObjectFactory +{ + public static void writeTo (DataOutputStream out, DObject dobj) + throws IOException + { + // first we write the class of the object to the stream + out.writeUTF(dobj.getClass().getName()); + // then we write the object itself + dobj.writeTo(out); + } + + public static DObject readFrom (DataInputStream in) + throws IOException + { + try { + Class clazz = Class.forName(in.readUTF()); + DObject dobj = (DObject)clazz.newInstance(); + dobj.readFrom(in); + return dobj; + + } catch (Exception e) { + String errmsg = "Unable to unserialize dobj: " + e; + throw new ObjectStreamException(errmsg); + } + } +} diff --git a/src/java/com/threerings/presents/io/FramedInputStream.java b/src/java/com/threerings/presents/io/FramedInputStream.java index dc6bbfbce..3a7db989b 100644 --- a/src/java/com/threerings/presents/io/FramedInputStream.java +++ b/src/java/com/threerings/presents/io/FramedInputStream.java @@ -1,5 +1,5 @@ // -// $Id: FramedInputStream.java,v 1.2 2001/05/22 22:01:08 mdb Exp $ +// $Id: FramedInputStream.java,v 1.3 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.io; @@ -17,19 +17,15 @@ import com.samskivert.util.StringUtil; * frame can be loaded from the network layer before any higher layer * attempts to process it. Additionally, any failure in decoding a frame * won't result in the entire stream being skewed due to the remainder of - * the undecoded frame. + * the undecoded frame remaining in the input stream. * *

The framed input stream reads an entire frame worth of data into its * internal buffer when readFrame() is called. It then * behaves as if this is the only data available on the stream (meaning * that when the data in the frame is exhausted, it will behave as if the - * end of the stream has been reached). A new frame can be read at any - * time and will be appended to the data available (the frame length data - * is never inserted into the stream data), but it is assumed that the - * caller will want to read and process an entire frame before going on to - * read the next frame (so that clear() can be called in the - * event of a frame decoding failure without clearing out the data from - * subsequent frames). + * end of the stream has been reached). The buffer can only contain a + * single frame at a time, so any data left over from a previous frame + * will disappear when readFrame() is called again. * *

Note: The framing input stream does not synchronize reads * from its internal buffer. It is intended to only be accessed from a @@ -48,65 +44,68 @@ public class FramedInputStream extends InputStream } /** - * Reads a single frame from the provided input stream and appends - * that data to the existing data available via the framed input - * stream's read methods. + * Reads a frame from the provided input stream, or appends to a + * partially read frame. Appends the read data to the existing data + * available via the framed input stream's read methods. If the entire + * frame data is not yet available, readFrame will return + * false, otherwise true. * - * @return the length of the read frame in bytes. + *

The code assumes that it will be able to read the entire frame + * header in a single read. The header is only four bytes and should + * always arrive at the beginning of a packet, so unless something is + * very funky with the networking layer, this should be a safe + * assumption. + * + * @return true if the entire frame has been read, false if the buffer + * contains only a partial frame. */ - public synchronized int readFrame (InputStream source) + public boolean readFrame (InputStream source) throws IOException { - // first read in the frame length - if (source.read(_header, 0, HEADER_SIZE) < HEADER_SIZE) { - throw new EOFException(); - } + // if the buffer currently contains a complete frame, that means + // we're not halfway through reading a frame and that we can start + // anew. + if (_count == _length) { + // clear out any prior data + _pos = 0; + _count = 0; - // now decode the frame length - int flength = (_header[0] << 24) & 0xFF; - flength += (_header[1] << 16) & 0xFF; - flength += (_header[2] << 8) & 0xFF; - flength += _header[3] & 0xFF; + // read in the frame length + int got = source.read(_header, 0, HEADER_SIZE); + if (got < 0) { + throw new EOFException(); - // expand our buffer to accomodate the frame data - int newcount = _count + flength; - if (newcount > _buffer.length) { - // increase the buffer size in large increments - byte[] newbuf = new byte[Math.max(_buffer.length << 1, newcount)]; - System.arraycopy(_buffer, 0, newbuf, 0, _count); - _buffer = newbuf; + } else if (got == 0) { + return false; + + } else if (got < HEADER_SIZE) { + String errmsg = "FramedInputStream does not support " + + "partially reading the header. Needed " + HEADER_SIZE + + " bytes, got " + got + " bytes."; + throw new RuntimeException(errmsg); + } + + // decode the frame length + _length = (_header[0] << 24) & 0xFF; + _length += (_header[1] << 16) & 0xFF; + _length += (_header[2] << 8) & 0xFF; + _length += _header[3] & 0xFF; + + // if necessary, expand our buffer to accomodate the frame + if (_length > _buffer.length) { + // increase the buffer size in large increments + _buffer = new byte[Math.max(_buffer.length << 1, _length)]; + } } // read the data into the buffer - if (source.read(_buffer, _count, flength) < flength) { + int got = source.read(_buffer, _count, _length); + if (got < 0) { throw new EOFException(); } - _count = newcount; + _count += got; - return flength; - } - - /** - * Clears out any previously read frame data and reads a new frame - * into the buffer. - * - * @return the length of the read frame in bytes. - */ - public int clearAndReadFrame (InputStream source) - throws IOException - { - clear(); - return readFrame(source); - } - - /** - * Clears out any frame data already in the buffer including anything - * that hasn't yet been read. - */ - public void clear () - { - _pos = 0; - _count = 0; + return (_count == _length); } /** @@ -240,6 +239,8 @@ public class FramedInputStream extends InputStream } protected byte[] _header; + protected int _length; + protected byte[] _buffer; protected int _pos; protected int _count; diff --git a/src/java/com/threerings/presents/io/FramingOutputStream.java b/src/java/com/threerings/presents/io/FramingOutputStream.java index 9dcef376e..69ad7a1e5 100644 --- a/src/java/com/threerings/presents/io/FramingOutputStream.java +++ b/src/java/com/threerings/presents/io/FramingOutputStream.java @@ -1,5 +1,5 @@ // -// $Id: FramingOutputStream.java,v 1.1 2001/05/22 21:51:29 mdb Exp $ +// $Id: FramingOutputStream.java,v 1.2 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.io; @@ -91,7 +91,7 @@ public class FramingOutputStream extends OutputStream * number of bytes written folling that integer. It then resets the * framing output stream to prepare for another framed message. */ - public synchronized void writeFrameAndReset (OutputStream target) + public void writeFrameAndReset (OutputStream target) throws IOException { // prefix the frame with the byte count in network byte order (the diff --git a/src/java/com/threerings/presents/net/AuthResponse.java b/src/java/com/threerings/presents/net/AuthResponse.java index e29fb0031..b903944af 100644 --- a/src/java/com/threerings/presents/net/AuthResponse.java +++ b/src/java/com/threerings/presents/net/AuthResponse.java @@ -1,5 +1,5 @@ // -// $Id: AuthResponse.java,v 1.2 2001/05/23 04:03:40 mdb Exp $ +// $Id: AuthResponse.java,v 1.3 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.net; @@ -7,7 +7,7 @@ import java.io.IOException; import java.io.DataInputStream; import java.io.DataOutputStream; -import com.samskivert.cocktail.cher.io.TypedObjectFactory; +import com.samskivert.cocktail.cher.dobj.DObjectFactory; /** * The auth response communicates authentication success or failure as @@ -51,14 +51,14 @@ public class AuthResponse extends DownstreamMessage throws IOException { super.writeTo(out); - TypedObjectFactory.writeTo(out, _data); + DObjectFactory.writeTo(out, _data); } public void readFrom (DataInputStream in) throws IOException { super.readFrom(in); - _data = (AuthResponseData)TypedObjectFactory.readFrom(in); + _data = (AuthResponseData)DObjectFactory.readFrom(in); } protected AuthResponseData _data; diff --git a/src/java/com/threerings/presents/net/AuthResponseData.java b/src/java/com/threerings/presents/net/AuthResponseData.java index 48363d0b3..6a941fff9 100644 --- a/src/java/com/threerings/presents/net/AuthResponseData.java +++ b/src/java/com/threerings/presents/net/AuthResponseData.java @@ -1,8 +1,12 @@ // -// $Id: AuthResponseData.java,v 1.1 2001/05/23 04:03:40 mdb Exp $ +// $Id: AuthResponseData.java,v 1.2 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.net; +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + import com.samskivert.cocktail.cher.dobj.DObject; /** @@ -18,4 +22,18 @@ public class AuthResponseData extends DObject * failed. */ public String code; + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + out.writeUTF(code); + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + code = in.readUTF(); + } } diff --git a/src/java/com/threerings/presents/net/Credentials.java b/src/java/com/threerings/presents/net/Credentials.java index d6499f43a..2d0ddcc4f 100644 --- a/src/java/com/threerings/presents/net/Credentials.java +++ b/src/java/com/threerings/presents/net/Credentials.java @@ -1,5 +1,5 @@ // -// $Id: Credentials.java,v 1.2 2001/05/23 04:03:40 mdb Exp $ +// $Id: Credentials.java,v 1.3 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.net; @@ -50,10 +50,4 @@ public abstract class Credentials implements TypedObject { // we don't do anything here, but we may want to some day } - - // register our credential classes - static { - TypedObjectFactory.registerClass(UsernamePasswordCreds.TYPE, - UsernamePasswordCreds.class); - } } diff --git a/src/java/com/threerings/presents/net/DownstreamMessage.java b/src/java/com/threerings/presents/net/DownstreamMessage.java index bd74d6a06..a7650090e 100644 --- a/src/java/com/threerings/presents/net/DownstreamMessage.java +++ b/src/java/com/threerings/presents/net/DownstreamMessage.java @@ -1,5 +1,5 @@ // -// $Id: DownstreamMessage.java,v 1.3 2001/05/23 04:03:40 mdb Exp $ +// $Id: DownstreamMessage.java,v 1.4 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.net; @@ -66,18 +66,4 @@ public abstract class DownstreamMessage implements TypedObject { // we don't do anything here, but we may want to some day } - - // register our downstream message classes - static { - TypedObjectFactory.registerClass(AuthResponse.TYPE, - AuthResponse.class); - TypedObjectFactory.registerClass(EventNotification.TYPE, - EventNotification.class); - TypedObjectFactory.registerClass(ObjectResponse.TYPE, - ObjectResponse.class); - TypedObjectFactory.registerClass(FailureResponse.TYPE, - FailureResponse.class); - TypedObjectFactory.registerClass(PongNotification.TYPE, - PongNotification.class); - } } diff --git a/src/java/com/threerings/presents/net/Registry.java b/src/java/com/threerings/presents/net/Registry.java new file mode 100644 index 000000000..737c33d9c --- /dev/null +++ b/src/java/com/threerings/presents/net/Registry.java @@ -0,0 +1,56 @@ +// +// $Id: Registry.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import com.samskivert.cocktail.cher.dobj.DObject; +import com.samskivert.cocktail.cher.io.TypedObject; +import com.samskivert.cocktail.cher.io.TypedObjectFactory; + +/** + * The registry provides a single place where all typed objects that are + * exchanged between the client and the server can be registered with the + * typed object factory. + */ +public class Registry +{ + /** + * Must be called once by the client and the server to ensure that all + * typed objects are registered with the typed object system. + */ + public static void registerTypedObjects () + { + // register our upstream message classes + TypedObjectFactory.registerClass(AuthRequest.TYPE, + AuthRequest.class); + TypedObjectFactory.registerClass(SubscribeRequest.TYPE, + SubscribeRequest.class); + TypedObjectFactory.registerClass(FetchRequest.TYPE, + FetchRequest.class); + TypedObjectFactory.registerClass(UnsubscribeNotification.TYPE, + UnsubscribeNotification.class); + TypedObjectFactory.registerClass(ForwardEventNotification.TYPE, + ForwardEventNotification.class); + TypedObjectFactory.registerClass(PingNotification.TYPE, + PingNotification.class); + TypedObjectFactory.registerClass(LogoffNotification.TYPE, + LogoffNotification.class); + + // register our downstream message classes + TypedObjectFactory.registerClass(AuthResponse.TYPE, + AuthResponse.class); + TypedObjectFactory.registerClass(EventNotification.TYPE, + EventNotification.class); + TypedObjectFactory.registerClass(ObjectResponse.TYPE, + ObjectResponse.class); + TypedObjectFactory.registerClass(FailureResponse.TYPE, + FailureResponse.class); + TypedObjectFactory.registerClass(PongNotification.TYPE, + PongNotification.class); + + // register our credential classes + TypedObjectFactory.registerClass(UsernamePasswordCreds.TYPE, + UsernamePasswordCreds.class); + + } +} diff --git a/src/java/com/threerings/presents/net/UpstreamMessage.java b/src/java/com/threerings/presents/net/UpstreamMessage.java index 4279c85d9..f3b50f165 100644 --- a/src/java/com/threerings/presents/net/UpstreamMessage.java +++ b/src/java/com/threerings/presents/net/UpstreamMessage.java @@ -1,5 +1,5 @@ // -// $Id: UpstreamMessage.java,v 1.3 2001/05/23 04:03:40 mdb Exp $ +// $Id: UpstreamMessage.java,v 1.4 2001/05/29 03:27:59 mdb Exp $ package com.samskivert.cocktail.cher.net; @@ -82,22 +82,4 @@ public abstract class UpstreamMessage implements TypedObject * the client as new messages are generated. */ protected static short _nextMessageId; - - // register our upstream message classes - static { - TypedObjectFactory.registerClass(AuthRequest.TYPE, - AuthRequest.class); - TypedObjectFactory.registerClass(SubscribeRequest.TYPE, - SubscribeRequest.class); - TypedObjectFactory.registerClass(FetchRequest.TYPE, - FetchRequest.class); - TypedObjectFactory.registerClass(UnsubscribeNotification.TYPE, - UnsubscribeNotification.class); - TypedObjectFactory.registerClass(ForwardEventNotification.TYPE, - ForwardEventNotification.class); - TypedObjectFactory.registerClass(PingNotification.TYPE, - PingNotification.class); - TypedObjectFactory.registerClass(LogoffNotification.TYPE, - LogoffNotification.class); - } } diff --git a/src/java/com/threerings/presents/server/Authenticator.java b/src/java/com/threerings/presents/server/Authenticator.java new file mode 100644 index 000000000..8a4e2fb32 --- /dev/null +++ b/src/java/com/threerings/presents/server/Authenticator.java @@ -0,0 +1,26 @@ +// +// $Id: Authenticator.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server.net; + +import com.samskivert.cocktail.cher.net.AuthRequest; +import com.samskivert.cocktail.cher.net.AuthResponse; + +/** + * The authenticator is a pluggable component of the authentication + * framework. It is provided with an auth request object and is expected + * to return an auth response object indicating whether or not the client + * is authenticated. authenticate() is invoked on the authmgr + * thread which means that it can access databases or other external + * (slow) information sources without concern for unduly blocking the + * normal operation of the server. Of course, authentication requests are + * processed serially, so they shouldn't take too long. + */ +public interface Authenticator +{ + /** + * Requests that the authenticator process the supplied request and + * provide a response object indicating success or failure. + */ + public AuthResponse process (AuthRequest request); +} diff --git a/src/java/com/threerings/presents/server/DummyAuthenticator.java b/src/java/com/threerings/presents/server/DummyAuthenticator.java new file mode 100644 index 000000000..5ded64cf8 --- /dev/null +++ b/src/java/com/threerings/presents/server/DummyAuthenticator.java @@ -0,0 +1,23 @@ +// +// $Id: DummyAuthenticator.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server; + +import com.samskivert.cocktail.cher.Log; +import com.samskivert.cocktail.cher.net.*; +import com.samskivert.cocktail.cher.server.net.Authenticator; +import com.samskivert.cocktail.cher.util.Codes; + +public class DummyAuthenticator implements Authenticator +{ + /** + * We just accept all authentication requests. + */ + public AuthResponse process (AuthRequest req) + { + Log.info("Accepting request: " + req); + AuthResponseData rdata = new AuthResponseData(); + rdata.code = Codes.SUCCESS; + return new AuthResponse(rdata); + } +} diff --git a/src/java/com/threerings/presents/server/PresentsServer.java b/src/java/com/threerings/presents/server/PresentsServer.java new file mode 100644 index 000000000..2ce344508 --- /dev/null +++ b/src/java/com/threerings/presents/server/PresentsServer.java @@ -0,0 +1,64 @@ +// +// $Id: PresentsServer.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server; + +import com.samskivert.cocktail.cher.Log; +import com.samskivert.cocktail.cher.server.net.AuthManager; +import com.samskivert.cocktail.cher.server.net.ConnectionManager; + +/** + * The cher server provides a central point of access to the various + * facilities that make up the cher layer of the system. + */ +public class CherServer +{ + /** The authentication manager. */ + public static AuthManager authmgr; + + /** The manager of network connections. */ + public static ConnectionManager cmgr; + + /** + * Initializes all of the server services and prepares for operation. + */ + public static void init () + { + try { + // create our authentication manager + authmgr = new AuthManager(new DummyAuthenticator()); + // create our connection manager + cmgr = new ConnectionManager(authmgr); + + } catch (Exception e) { + Log.warning("Unable to initialize server."); + Log.logStackTrace(e); + } + } + + /** + * Starts up all of the server services and enters the main server + * event loop. + */ + public static void start () + { + // start up the auth manager + authmgr.start(); + // start up the connection manager + cmgr.start(); + + // for now, just block because we've nothing to do + try { + synchronized (CherServer.class) { + CherServer.class.wait(); + } + } catch (InterruptedException ie) { + } + } + + public static void main (String[] args) + { + init(); + start(); + } +} diff --git a/src/java/com/threerings/presents/server/net/AuthManager.java b/src/java/com/threerings/presents/server/net/AuthManager.java new file mode 100644 index 000000000..124d6aa90 --- /dev/null +++ b/src/java/com/threerings/presents/server/net/AuthManager.java @@ -0,0 +1,65 @@ +// +// $Id: AuthManager.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server.net; + +import com.samskivert.util.LoopingThread; +import com.samskivert.util.Queue; + +import com.samskivert.cocktail.cher.Log; +import com.samskivert.cocktail.cher.net.AuthRequest; +import com.samskivert.cocktail.cher.net.AuthResponse; + +/** + * The authentication manager takes care of the authentication process. + * Authentication happens on multiple threads. The conmgr thread parses + * the authentication request and passes it on to the authmgr thread. The + * authmgr thread invokes the (pluggable) authenticator to do the actual + * authentication. Then the response message is queued up to be delivered + * by the conmgr thread. + * + *

This structure prevents authentication to take place + * asynchronously, but in a controlled manner. Only one authentication + * will be processed at a time, but the dobj and conmgr threads will + * continue to operate independent of the authentication process. + */ +public class AuthManager extends LoopingThread +{ + public AuthManager (Authenticator author) + { + _author = author; + } + + /** + * Puts an authenticating connection on the queue. The connection will + * be authenticated and an auth response delivered. + */ + public void postAuthingConnection (AuthingConnection aconn) + { + _authq.append(aconn); + } + + /** + * Process auth requests. + */ + protected void iterate () + { + // grab the next authing connection from the queue + AuthingConnection aconn = (AuthingConnection)_authq.get(); + + try { + // instruct the authenticator to process the auth request + AuthResponse rsp = _author.process(aconn.getAuthRequest()); + + // now ship the response back + aconn.postMessage(rsp); + + } catch (Exception e) { + Log.warning("Failure processing authreq [conn=" + aconn + "]."); + Log.logStackTrace(e); + } + } + + protected Authenticator _author; + protected Queue _authq = new Queue(); +} diff --git a/src/java/com/threerings/presents/server/net/AuthingConnection.java b/src/java/com/threerings/presents/server/net/AuthingConnection.java new file mode 100644 index 000000000..ca3f3bf63 --- /dev/null +++ b/src/java/com/threerings/presents/server/net/AuthingConnection.java @@ -0,0 +1,63 @@ +// +// $Id: AuthingConnection.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server.net; + +import java.io.IOException; +import ninja2.core.io_core.nbio.NonblockingSocket; + +import com.samskivert.cocktail.cher.Log; +import com.samskivert.cocktail.cher.net.UpstreamMessage; +import com.samskivert.cocktail.cher.net.AuthRequest; + +/** + * The authing connection manages the client connection until + * authentication has completed (for better or for worse). + */ +public class AuthingConnection extends Connection +{ + /** + * Creates a new authing connection object that will manage the + * authentication process for the suppled client socket. + */ + public AuthingConnection (ConnectionManager cmgr, + NonblockingSocket socket) + throws IOException + { + super(cmgr, socket); + } + + /** + * Called when a new message has arrived from the client. + */ + public void handleMessage (UpstreamMessage msg) + { + try { + // keep a handle on our auth request + _authreq = (AuthRequest)msg; + + // post ourselves for processing by the authmgr + _cmgr.getAuthManager().postAuthingConnection(this); + + } catch (ClassCastException cce) { + Log.warning("Received non-authreq message during " + + "authentication process [conn=" + this + + ", msg=" + msg + "]."); + } + } + + /** + * Returns a reference to the auth request currently being processed + * by this authing connection. + */ + public AuthRequest getAuthRequest () + { + return _authreq; + } + + protected int _state = AWAITING_AUTH_REQUEST; + protected AuthRequest _authreq; + + protected static final int AWAITING_AUTH_REQUEST = 0; + protected static final int PROCESSING_AUTH_REQUEST = 1; +} diff --git a/src/java/com/threerings/presents/server/net/Connection.java b/src/java/com/threerings/presents/server/net/Connection.java new file mode 100644 index 000000000..efdaa1989 --- /dev/null +++ b/src/java/com/threerings/presents/server/net/Connection.java @@ -0,0 +1,115 @@ +// +// $Id: Connection.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server.net; + +import java.io.*; +import ninja2.core.io_core.nbio.*; + +import com.samskivert.cocktail.cher.Log; +import com.samskivert.cocktail.cher.io.FramedInputStream; +import com.samskivert.cocktail.cher.io.TypedObjectFactory; +import com.samskivert.cocktail.cher.net.UpstreamMessage; +import com.samskivert.cocktail.cher.net.DownstreamMessage; + +/** + * The base connection class implements the net event handler interface + * and processes raw incoming network data into a stream of parsed + * UpstreamMessage objects. It also provides the means to + * send messages to the client and facilities for checking delinquency. + */ +public abstract class Connection implements NetEventHandler +{ + /** + * Constructs a connection object that is associated with the supplied + * socket. + * + * @param cmgr The connection manager with which this connection is + * associated. + * @param socket The socket from which we'll be reading messages. + */ + public Connection (ConnectionManager cmgr, NonblockingSocket socket) + throws IOException + { + _cmgr = cmgr; + _socket = socket; + + // create a select item that will allow us to be incorporated into + // the main event polling loop + _selitem = new SelectItem(_socket, this, Selectable.READ_READY); + + // create our input streams + _in = _socket.getInputStream(); + _out = _socket.getOutputStream(); + _fin = new FramedInputStream(); + _din = new DataInputStream(_fin); + } + + /** + * Returns the select item associated with this connection. + */ + public SelectItem getSelectItem () + { + return _selitem; + } + + /** + * Returns the output stream associated with this connection. This + * should only be used by the connection manager. + */ + public OutputStream getOutputStream () + { + return _out; + } + + /** + * Called when our client socket has data available for reading. + */ + public void handleEvent (Selectable source, short events) + { + try { + // read the available data and see if we have a whole frame + if (_fin.readFrame(_in)) { + // parse the message and pass it on + handleMessage((UpstreamMessage) + TypedObjectFactory.readFrom(_din)); + } + + } catch (EOFException eofe) { + // let the connection manager know that we done went away + _cmgr.connectionClosed(this); + + } catch (IOException ioe) { + Log.warning("Error reading message from socket " + + "[socket=" + _socket + ", error=" + ioe + "]."); + // let the connection manager know that something when awry + _cmgr.connectionFailed(this, ioe); + } + } + + /** + * Called when a complete message has been parsed from incoming + * network data. + */ + public abstract void handleMessage (UpstreamMessage msg); + + /** + * Posts a downstream message for delivery to this connection. The + * message will be delivered by the conmgr thread as soon as it gets + * to it. + */ + public void postMessage (DownstreamMessage msg) + { + // pass this along to the connection manager + _cmgr.postMessage(this, msg); + } + + protected ConnectionManager _cmgr; + protected NonblockingSocket _socket; + protected SelectItem _selitem; + + protected InputStream _in; + protected OutputStream _out; + protected FramedInputStream _fin; + protected DataInputStream _din; +} diff --git a/src/java/com/threerings/presents/server/net/ConnectionManager.java b/src/java/com/threerings/presents/server/net/ConnectionManager.java new file mode 100644 index 000000000..39393c156 --- /dev/null +++ b/src/java/com/threerings/presents/server/net/ConnectionManager.java @@ -0,0 +1,226 @@ +// +// $Id: ConnectionManager.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server.net; + +import java.io.DataOutputStream; +import java.io.IOException; +import ninja2.core.io_core.nbio.*; + +import com.samskivert.util.LoopingThread; +import com.samskivert.util.Tuple; +import com.samskivert.util.Queue; + +import com.samskivert.cocktail.cher.Log; +import com.samskivert.cocktail.cher.io.FramingOutputStream; +import com.samskivert.cocktail.cher.io.TypedObjectFactory; +import com.samskivert.cocktail.cher.net.DownstreamMessage; +import com.samskivert.cocktail.cher.net.Registry; + +/** + * The connection manager manages the socket on which connections are + * received. It creates connection objects to manage each individual + * connection, but those connection objects interact closely with the + * connection manager because network I/O is done via a poll()-like + * mechanism rather than via threads. + */ +public class ConnectionManager extends LoopingThread +{ + /** + * Constructs and initialized a connection manager (binding the socket + * on which it will listen for client connections). + * + * @param authmgr The authentication manager to use when + * authenticating client connections. + */ + public ConnectionManager (AuthManager authmgr) + throws IOException + { + int port = DEFAULT_CM_PORT; + + // keep a handle on our authentication manager + _authmgr = authmgr; + + // we use this to wait for activity on our sockets + _selset = new SelectSet(); + + // create our listening socket and add it to the select set + _listener = new NonblockingServerSocket(port); + _litem = new SelectItem(_listener, Selectable.ACCEPT_READY); + // when an ACCEPT_READY event happens, we do this: + _litem.obj = new NetEventHandler() { + public void handleEvent (Selectable item, short events) { + acceptConnection(); + } + }; + _selset.add(_litem); + + // we'll use these for sending messages to clients + _framer = new FramingOutputStream(); + _dout = new DataOutputStream(_framer); + } + + /** + * Returns a reference to the auth manager being used to authenticate + * connections. + */ + public AuthManager getAuthManager () + { + return _authmgr; + } + + protected void willStart () + { + Log.info("Connection Manager thread running."); + } + + /** + * Performs the select loop. This is the body of the conmgr thread. + */ + protected void iterate () + { + // send any messages that are waiting on the outgoing queue + Tuple tup; + while ((tup = (Tuple)_outq.getNonBlocking()) != null) { + Connection conn = (Connection)tup.left; + DownstreamMessage outmsg = (DownstreamMessage)tup.right; + try { + // first flatten the message (and frame it) + TypedObjectFactory.writeTo(_dout, outmsg); + // then write framed message to real output stream + _framer.writeFrameAndReset(conn.getOutputStream()); + + } catch (IOException ioe) { + connectionFailed(conn, ioe); + } + } + + // check for incoming network events + int ecount = _selset.select(SELECT_LOOP_TIME); + if (ecount == 0) { + return; + } + + // process those events + SelectItem[] active = _selset.getEvents(); + for (int i = 0; i < active.length; i++) { + try { + SelectItem item = active[i]; + NetEventHandler handler = (NetEventHandler)item.obj; + handler.handleEvent(item.getSelectable(), item.revents); + + } catch (Exception e) { + Log.warning("Error processing network data."); + Log.logStackTrace(e); + } + } + } + + protected void didShutdown () + { + Log.info("Connection Manager thread exited."); + } + + /** + * Called by our net event handler when a new connection is ready to + * be accepted on our listening socket. + */ + protected void acceptConnection () + { + NonblockingSocket socket = null; + + try { + socket = _listener.nbAccept(); + if (socket == null) { + // in theory this shouldn't happen because we got an + // ACCEPT_READY event, but better safe than sorry + Log.info("Psych! Got ACCEPT_READY, but no connection."); + return; + } + + // create a new authing connection object to manage the + // authentication of this client connection + AuthingConnection acon = new AuthingConnection(this, socket); + + // wire this connection into the select set + _selset.add(acon.getSelectItem()); + + } catch (IOException ioe) { + Log.warning("Failure accepting new connection: " + ioe); + + // make sure we don't leak a socket if something went awry + if (socket != null) { + try { + socket.close(); + } catch (IOException ioe2) { + Log.warning("Failed closing aborted connection: " + ioe2); + } + } + } + } + + /** + * Called by a connection when it has a downstream message that needs + * to be delivered. + */ + void postMessage (Connection conn, DownstreamMessage msg) + { + // sanity check + if (conn == null || msg == null) { + Log.warning("Bogosity."); + Thread.dumpStack(); + + } else { + // slap both these suckers onto the outgoing message queue + _outq.append(new Tuple(conn, msg)); + } + } + + /** + * Called by a connection if it experiences a network failure. + */ + void connectionFailed (Connection conn, IOException ioe) + { + // remove this connection from the select set + _selset.remove(conn.getSelectItem()); + + // let our observers know what's up + } + + /** + * Called by a connection when it discovers that it's closed. + */ + void connectionClosed (Connection conn) + { + Log.info("Removing closed connection: " + conn); + + // remove this connection from the select set + _selset.remove(conn.getSelectItem()); + + // let our observers know what's up + } + + protected AuthManager _authmgr; + protected SelectSet _selset; + + protected NonblockingServerSocket _listener; + protected SelectItem _litem; + + protected Queue _outq = new Queue(); + protected FramingOutputStream _framer; + protected DataOutputStream _dout; + + /** The default port on which we listen for connections. */ + protected static final int DEFAULT_CM_PORT = 4007; + + /** + * How long we wait for network events before checking our running + * flag to see if we should still be running. + */ + protected static final int SELECT_LOOP_TIME = 30 * 1000; + + // register our shared objects + static { + Registry.registerTypedObjects(); + } +} diff --git a/src/java/com/threerings/presents/server/net/ConnectionObserver.java b/src/java/com/threerings/presents/server/net/ConnectionObserver.java new file mode 100644 index 000000000..34a19501f --- /dev/null +++ b/src/java/com/threerings/presents/server/net/ConnectionObserver.java @@ -0,0 +1,45 @@ +// +// $Id: ConnectionObserver.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server.net; + +import java.io.IOException; + +/** + * A connection observer can be registered with the connection manager to + * hear about new connections and to be notified when connections fail or + * are closed. Only fully authenticated connections will be passed on to + * the connection observer. Connections that fail to authenticate will be + * handled entirely within the confines of the connection manager. + * + * @see ConnectionManager + * @see Connection + */ +public interface ConnectionObserver +{ + /** + * Called when a new connection is established with the connection + * manager. + * + * @param conn The newly established connection. + */ + public void connectionEstablished (Connection conn); + + /** + * Called if a connection fails for any reason. If a connection fails, + * connectionClosed will not be called. This call to + * connectionFailued is the last the observers will hear + * about it. + * + * @param conn The connection in that failed. + * @param fault The exception associated with the failure. + */ + public void connectionFailed (Connection conn, IOException fault); + + /** + * Called when a connection has been closed in an orderly manner. + * + * @param conn The recently closed connection. + */ + public void connectionClosed (Connection conn); +} diff --git a/src/java/com/threerings/presents/server/net/NetEventHandler.java b/src/java/com/threerings/presents/server/net/NetEventHandler.java new file mode 100644 index 000000000..e930f633c --- /dev/null +++ b/src/java/com/threerings/presents/server/net/NetEventHandler.java @@ -0,0 +1,27 @@ +// +// $Id: NetEventHandler.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server.net; + +import ninja2.core.io_core.nbio.Selectable; + +/** + * When a network event arrives on a particular Selectable, + * the connection manager calls the net event handler associated with that + * selectable to process the event. There are only a few handlers (and + * probably only ever will be): the one that accepts new connections, the + * one that deals with a connection while the client is authenticating and + * the one that processes messages from authenticated connections. + * Providing this interface prevents us from having to do a bunch of + * inefficient and ugly comparisons; instead we can simply call through an + * interface method to the proper code. + */ +public interface NetEventHandler +{ + /** + * Called when a network event has occurred on the supplied source. + * The events parameter indicates which event or events + * have occurred. + */ + public void handleEvent (Selectable source, short events); +} diff --git a/src/java/com/threerings/presents/server/net/RunningConnection.java b/src/java/com/threerings/presents/server/net/RunningConnection.java new file mode 100644 index 000000000..79d437b8c --- /dev/null +++ b/src/java/com/threerings/presents/server/net/RunningConnection.java @@ -0,0 +1,33 @@ +// +// $Id: RunningConnection.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.server.net; + +import java.io.IOException; +import ninja2.core.io_core.nbio.NonblockingSocket; +import com.samskivert.cocktail.cher.net.UpstreamMessage; + +/** + * A running connection object represents a fully operational client + * connection to the server. + */ +public class RunningConnection extends Connection +{ + /** + * Constructs a new running connection object to manage the supplied + * client socket. + */ + public RunningConnection (ConnectionManager cmgr, + NonblockingSocket socket) + throws IOException + { + super(cmgr, socket); + } + + /** + * Called when a new message has arrived from the client. + */ + public void handleMessage (UpstreamMessage msg) + { + } +} diff --git a/tests/src/java/com/threerings/presents/client/TestClient.java b/tests/src/java/com/threerings/presents/client/TestClient.java new file mode 100644 index 000000000..4b71ba2a3 --- /dev/null +++ b/tests/src/java/com/threerings/presents/client/TestClient.java @@ -0,0 +1,22 @@ +// +// $Id: TestClient.java,v 1.1 2001/05/29 03:27:59 mdb Exp $ + +package com.samskivert.cocktail.cher.client.test; + +import com.samskivert.cocktail.cher.net.*; +import com.samskivert.cocktail.cher.client.*; + +/** + * A standalone test client. + */ +public class TestClient +{ + public static void main (String[] args) + { + UsernamePasswordCreds creds = + new UsernamePasswordCreds("test", "test"); + Client client = new Client(creds); + client.setServer("localhost", 4007); + client.logon(); + } +}