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();
+ }
+}