Revamping/cleanup of how we handle authentication usernames as well as chained
authenticators and chained session factories. We can share a lot more code this way and the implicit requirement that the default authenticator/factory had to be configured before anyone else configured a chanied author/factory is gone. The other big change is that Credentials doesn't require a username (UsernamePasswordCreds inherits that username so most derived classes don't notice any difference). Instead we require that a canonical authentication username be determined and configured in AuthingConnection during the authentication process. This canonical username is then used to resolve the client session and map everything in the client manager. This is pretty much exactly what was going on before except that we were doing it all in an ad hoc way by jamming a new name into Credentials during the authentication process and also doing jiggery pokery in PresentsSession.assignStartingUsername. That all goes away and/or becomes cleaner and more explicit. This is going to impact some Yohoho jiggery pokery, which I will shortly commit a patch for, but we're going to need to test it. Omelets, eggs, etc. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5828 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
@@ -55,6 +55,10 @@ public abstract class Authenticator
|
||||
public boolean invoke () {
|
||||
try {
|
||||
processAuthentication(conn, rsp);
|
||||
if (AuthResponseData.SUCCESS.equals(rdata.code) &&
|
||||
conn.getAuthName() == null) { // fail early, fail (less) often
|
||||
throw new IllegalStateException("Authenticator failed to provide authname");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warning("Error authenticating user", "areq", req, e);
|
||||
rdata.code = AuthCodes.SERVER_ERROR;
|
||||
|
||||
@@ -21,11 +21,7 @@
|
||||
|
||||
package com.threerings.presents.server;
|
||||
|
||||
import com.samskivert.util.Invoker;
|
||||
import com.samskivert.util.ResultListener;
|
||||
|
||||
import com.threerings.presents.server.net.AuthingConnection;
|
||||
import com.threerings.presents.server.net.ConnectionManager;
|
||||
|
||||
/**
|
||||
* Handles certain special kinds of authentications and passes the remainder through to the default
|
||||
@@ -33,33 +29,9 @@ import com.threerings.presents.server.net.ConnectionManager;
|
||||
*/
|
||||
public abstract class ChainedAuthenticator extends Authenticator
|
||||
{
|
||||
/**
|
||||
* Called by the {@link ConnectionManager} to initialize our delegate.
|
||||
*/
|
||||
public void setChainedAuthenticator (Authenticator author)
|
||||
{
|
||||
_delegate = author;
|
||||
}
|
||||
|
||||
@Override // from Authenticator
|
||||
public void authenticateConnection (Invoker invoker, AuthingConnection conn,
|
||||
ResultListener<AuthingConnection> onComplete)
|
||||
{
|
||||
// if we handle this sort of authentication, then do so
|
||||
if (shouldHandleConnection(conn)) {
|
||||
super.authenticateConnection(invoker, conn, onComplete);
|
||||
|
||||
} else {
|
||||
// otherwise pass the request on to our delegate
|
||||
_delegate.authenticateConnection(invoker, conn, onComplete);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived classes should implement this method and return true if the supplied connection is
|
||||
* one that they should authenticate.
|
||||
*/
|
||||
protected abstract boolean shouldHandleConnection (AuthingConnection conn);
|
||||
|
||||
protected Authenticator _delegate;
|
||||
public abstract boolean shouldHandleConnection (AuthingConnection conn);
|
||||
}
|
||||
|
||||
@@ -117,20 +117,23 @@ public class ClientManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client manager with a factory for creating {@link PresentsSession} and {@link
|
||||
* ClientResolver} classes for authenticated client connections.
|
||||
* Configures the default factory for creating {@link PresentsSession} and {@link
|
||||
* ClientResolver} classes for authenticated client connections. All factories added via {@link
|
||||
* #addSessionFactory} will be offered a chance to handle sessions before this factory of last
|
||||
* resort.
|
||||
*/
|
||||
public void setSessionFactory (SessionFactory factory)
|
||||
public void setDefaultSessionFactory (SessionFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_factories.set(_factories.size()-1, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link SessionFactory} currently in use.
|
||||
* Adds a session factory to the chain. This factory will be offered a chance to resolve
|
||||
* sessions before passing the buck to the next factory in the chain.
|
||||
*/
|
||||
public SessionFactory getSessionFactory ()
|
||||
public void addSessionFactory (SessionFactory factory)
|
||||
{
|
||||
return _factory;
|
||||
_factories.add(0, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,10 +274,18 @@ public class ClientManager
|
||||
return;
|
||||
}
|
||||
|
||||
// figure out our client resolver class
|
||||
Class<? extends ClientResolver> resolverClass = null;
|
||||
for (SessionFactory factory : _factories) {
|
||||
if ((resolverClass = factory.getClientResolverClass(username)) != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// create a client resolver instance which will create our client object, populate it
|
||||
// and notify the listeners
|
||||
clr = _injector.getInstance(_factory.getClientResolverClass(username));
|
||||
clr = _injector.getInstance(resolverClass);
|
||||
clr.init(username);
|
||||
clr.addResolutionListener(this);
|
||||
clr.addResolutionListener(listener);
|
||||
@@ -400,28 +411,34 @@ public class ClientManager
|
||||
* Called by the connection manager to let us know when a new connection has been established.
|
||||
*/
|
||||
public synchronized void connectionEstablished (
|
||||
Connection conn, AuthRequest req, AuthResponse rsp)
|
||||
Connection conn, Name authname, AuthRequest req, AuthResponse rsp)
|
||||
{
|
||||
Credentials creds = req.getCredentials();
|
||||
Name username = creds.getUsername();
|
||||
String type = username.getClass().getSimpleName();
|
||||
String type = authname.getClass().getSimpleName();
|
||||
|
||||
// see if a client is already registered with these credentials
|
||||
PresentsSession client = getClient(username);
|
||||
// see if a client is already registered with this name
|
||||
PresentsSession client = getClient(authname);
|
||||
|
||||
if (client != null) {
|
||||
log.info("Resuming session", "type", type, "who", username, "conn", conn);
|
||||
log.info("Resuming session", "type", type, "who", authname, "conn", conn);
|
||||
client.resumeSession(req, conn);
|
||||
|
||||
} else {
|
||||
log.info("Session initiated", "type", type, "who", username, "conn", conn);
|
||||
log.info("Session initiated", "type", type, "who", authname, "conn", conn);
|
||||
// figure out our session class
|
||||
Class<? extends PresentsSession> sessionClass = null;
|
||||
for (SessionFactory factory : _factories) {
|
||||
if ((sessionClass = factory.getSessionClass(req)) != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// create a new client and stick'em in the table
|
||||
client = _injector.getInstance(_factory.getSessionClass(req));
|
||||
client.startSession(req, conn, rsp.authdata);
|
||||
client = _injector.getInstance(sessionClass);
|
||||
client.startSession(authname, req, conn, rsp.authdata);
|
||||
|
||||
// map their client instance
|
||||
synchronized (_usermap) {
|
||||
_usermap.put(username, client);
|
||||
_usermap.put(authname, client);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,10 +532,9 @@ public class ClientManager
|
||||
protected void clearSession (PresentsSession session)
|
||||
{
|
||||
// remove the client from the username map
|
||||
Name username = session.getCredentials().getUsername();
|
||||
PresentsSession rc;
|
||||
synchronized (_usermap) {
|
||||
rc = _usermap.remove(username);
|
||||
rc = _usermap.remove(session.getUsername());
|
||||
}
|
||||
|
||||
// sanity check just because we can
|
||||
@@ -604,7 +620,7 @@ public class ClientManager
|
||||
protected Map<Name, ClientResolver> _penders = Maps.newHashMap();
|
||||
|
||||
/** Lets us know what sort of client classes to use. */
|
||||
protected SessionFactory _factory = SessionFactory.DEFAULT;
|
||||
protected List<SessionFactory> _factories = Lists.newArrayList(SessionFactory.DEFAULT);
|
||||
|
||||
/** Tracks registered {@link ClientObserver}s. */
|
||||
protected ObserverList<ClientObserver> _clobservers = ObserverList.newSafeInOrder();
|
||||
|
||||
@@ -23,8 +23,12 @@ package com.threerings.presents.server;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
|
||||
import com.threerings.util.Name;
|
||||
|
||||
import com.threerings.presents.net.AuthResponse;
|
||||
import com.threerings.presents.net.AuthResponseData;
|
||||
import com.threerings.presents.net.Credentials;
|
||||
import com.threerings.presents.net.UsernamePasswordCreds;
|
||||
import com.threerings.presents.server.net.AuthingConnection;
|
||||
|
||||
import static com.threerings.presents.Log.log;
|
||||
@@ -39,6 +43,15 @@ public class DummyAuthenticator extends Authenticator
|
||||
throws PersistenceException
|
||||
{
|
||||
log.info("Accepting request: " + conn.getAuthRequest());
|
||||
|
||||
// we need to provide some sort of authentication username
|
||||
Credentials creds = conn.getAuthRequest().getCredentials();
|
||||
if (creds instanceof UsernamePasswordCreds) {
|
||||
conn.setAuthName(((UsernamePasswordCreds)creds).getUsername());
|
||||
} else {
|
||||
conn.setAuthName(new Name(conn.getInetAddress().getHostAddress()));
|
||||
}
|
||||
|
||||
rsp.getData().code = AuthResponseData.SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,15 +465,13 @@ public class PresentsSession
|
||||
* Initializes this client instance with the specified username, connection instance and client
|
||||
* object and begins a client session.
|
||||
*/
|
||||
protected void startSession (AuthRequest req, Connection conn, Object authdata)
|
||||
protected void startSession (Name authname, AuthRequest req, Connection conn, Object authdata)
|
||||
{
|
||||
_username = authname;
|
||||
_areq = req;
|
||||
_authdata = authdata;
|
||||
setConnection(conn);
|
||||
|
||||
// obtain our starting username
|
||||
assignStartingUsername();
|
||||
|
||||
// resolve our client object before we get fully underway
|
||||
_clmgr.resolveClientObject(_username, this);
|
||||
|
||||
@@ -481,17 +479,6 @@ public class PresentsSession
|
||||
_sessionStamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is factored out to allow derived classes to use a different starting username than the
|
||||
* one supplied in the user's credentials. Generally one only wants to munge the starting
|
||||
* username if the user will subsequently choose a "screen name" and it is desirable to avoid
|
||||
* collision between the authentication user namespace and the screen namespace.
|
||||
*/
|
||||
protected void assignStartingUsername ()
|
||||
{
|
||||
_username = getCredentials().getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the client manager when a new connection arrives that authenticates as this
|
||||
* already established client. This must only be called from the congmr thread.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2009 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Lesser General Public License as published
|
||||
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
package com.threerings.presents.server;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
import com.samskivert.io.PersistenceException;
|
||||
|
||||
import com.threerings.util.Name;
|
||||
|
||||
import com.threerings.presents.data.AuthCodes;
|
||||
import com.threerings.presents.net.AuthResponse;
|
||||
import com.threerings.presents.net.AuthResponseData;
|
||||
import com.threerings.presents.net.ServiceCreds;
|
||||
import com.threerings.presents.server.net.AuthingConnection;
|
||||
|
||||
import static com.threerings.presents.Log.log;
|
||||
|
||||
/**
|
||||
* Works in conjunction with {@link ServiceCreds} to handle the authentication of service clients
|
||||
* (bureaus, peers, etc.).
|
||||
*/
|
||||
public abstract class ServiceAuthenticator<T extends ServiceCreds> extends ChainedAuthenticator
|
||||
{
|
||||
/**
|
||||
* Creates an authenticator that will handle requests using the supplied credentials class and
|
||||
* which will create instances of the supplied auth name class to identify those clients. Note
|
||||
* that the auth name class <em>must</em> have a public constructor that takes a single string.
|
||||
*/
|
||||
public ServiceAuthenticator (Class<T> credsClass, Class<? extends Name> authNameClass)
|
||||
{
|
||||
_credsClass = credsClass;
|
||||
try {
|
||||
_authNamer = authNameClass.getConstructor(String.class);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
throw new IllegalArgumentException("AuthName must have AuthName(String) constructor.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override // from abstract ChainedAuthenticator
|
||||
public boolean shouldHandleConnection (AuthingConnection conn)
|
||||
{
|
||||
return _credsClass.isInstance(conn.getAuthRequest().getCredentials());
|
||||
}
|
||||
|
||||
@Override // from abstract Authenticator
|
||||
protected void processAuthentication (AuthingConnection conn, AuthResponse rsp)
|
||||
throws PersistenceException
|
||||
{
|
||||
T creds = _credsClass.cast(conn.getAuthRequest().getCredentials());
|
||||
if (!areValid(creds)) {
|
||||
log.warning("Received invalid service auth request?", "creds", creds);
|
||||
rsp.getData().code = AuthCodes.SERVER_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
conn.setAuthName(_authNamer.newInstance(creds.clientId));
|
||||
rsp.getData().code = AuthResponseData.SUCCESS;
|
||||
} catch (Exception e) {
|
||||
log.warning("Failed to construct auth name", "namer", _authNamer, e);
|
||||
rsp.getData().code = AuthCodes.SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the creds in question are valid.
|
||||
*/
|
||||
protected abstract boolean areValid (T creds);
|
||||
|
||||
protected Class<T> _credsClass;
|
||||
protected Constructor<? extends Name> _authNamer;
|
||||
}
|
||||
@@ -24,12 +24,13 @@ package com.threerings.presents.server;
|
||||
import com.threerings.util.Name;
|
||||
|
||||
import com.threerings.presents.net.AuthRequest;
|
||||
import com.threerings.presents.net.Credentials;
|
||||
|
||||
/**
|
||||
* Used to determine what type of {@link PresentsSession} to use to manage an authenticated client
|
||||
* as well the type of {@link ClientResolver} to use when resolving clients' runtime data.
|
||||
*/
|
||||
public interface SessionFactory
|
||||
public abstract class SessionFactory
|
||||
{
|
||||
/** The default client factory. */
|
||||
public static SessionFactory DEFAULT = new SessionFactory() {
|
||||
@@ -42,14 +43,35 @@ public interface SessionFactory
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the {@link PresentsSession} derived class to use for the session that authenticated
|
||||
* with the supplied request.
|
||||
* Creates a session factory that handles clients with the supplied credentials and
|
||||
* authentication name.
|
||||
*/
|
||||
Class<? extends PresentsSession> getSessionClass (AuthRequest areq);
|
||||
public static SessionFactory newSessionFactory (
|
||||
final Class<? extends Credentials> credsClass,
|
||||
final Class<? extends PresentsSession> sessionClass,
|
||||
final Class<? extends Name> nameClass,
|
||||
final Class<? extends ClientResolver> resolverClass)
|
||||
{
|
||||
return new SessionFactory() {
|
||||
public Class<? extends PresentsSession> getSessionClass (AuthRequest areq) {
|
||||
return credsClass.isInstance(areq.getCredentials()) ? sessionClass : null;
|
||||
}
|
||||
public Class <? extends ClientResolver> getClientResolverClass (Name username) {
|
||||
return nameClass.isInstance(username) ? resolverClass : null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PresentsSession} derived class to use for the session that authenticated
|
||||
* with the supplied request or null if this factory does not handle sessions of the supplied
|
||||
* type.
|
||||
*/
|
||||
public abstract Class<? extends PresentsSession> getSessionClass (AuthRequest areq);
|
||||
|
||||
/**
|
||||
* Returns the {@link ClientResolver} derived class to use to resolve a client with the
|
||||
* specified username.
|
||||
* specified username or null if this factory does not handle clients of the supplied type.
|
||||
*/
|
||||
Class <? extends ClientResolver> getClientResolverClass (Name username);
|
||||
public abstract Class <? extends ClientResolver> getClientResolverClass (Name username);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
package com.threerings.presents.server.net;
|
||||
|
||||
import com.threerings.util.Name;
|
||||
|
||||
import com.threerings.presents.net.AuthRequest;
|
||||
import com.threerings.presents.net.AuthResponse;
|
||||
import com.threerings.presents.net.Message;
|
||||
@@ -76,6 +78,24 @@ public class AuthingConnection extends Connection
|
||||
_authrsp = authrsp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username that uniquely identifies this authenticated session. This will be used
|
||||
* to map Name -> PresentsSession in the ClientManager and used elsewhere.
|
||||
*/
|
||||
public Name getAuthName ()
|
||||
{
|
||||
return _authname;
|
||||
}
|
||||
|
||||
/**
|
||||
* During the authentication process, the authenticator must establish the client's
|
||||
* authentication username and configure it via this method.
|
||||
*/
|
||||
public void setAuthName (Name authname)
|
||||
{
|
||||
_authname = authname;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString ()
|
||||
{
|
||||
@@ -84,4 +104,5 @@ public class AuthingConnection extends Connection
|
||||
|
||||
protected AuthRequest _authreq;
|
||||
protected AuthResponse _authrsp;
|
||||
protected Name _authname;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ package com.threerings.presents.server.net;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -39,6 +40,7 @@ import java.nio.channels.ServerSocketChannel;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.channels.spi.SelectorProvider;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
@@ -134,12 +136,11 @@ public class ConnectionManager extends LoopingThread
|
||||
|
||||
/**
|
||||
* Adds an authenticator to the authentication chain. This authenticator will be offered a
|
||||
* chance to authenticate incoming connections in lieu of the main autuenticator.
|
||||
* chance to authenticate incoming connections before falling back to the main authenticator.
|
||||
*/
|
||||
public void addChainedAuthenticator (ChainedAuthenticator author)
|
||||
{
|
||||
author.setChainedAuthenticator(_author);
|
||||
_author = author;
|
||||
_authors.add(author);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,7 +267,14 @@ public class ConnectionManager extends LoopingThread
|
||||
*/
|
||||
protected void authenticateConnection (AuthingConnection conn)
|
||||
{
|
||||
_author.authenticateConnection(_authInvoker, conn, new ResultListener<AuthingConnection>() {
|
||||
Authenticator author = _author;
|
||||
for (ChainedAuthenticator cauthor : _authors) {
|
||||
if (cauthor.shouldHandleConnection(conn)) {
|
||||
author = cauthor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
author.authenticateConnection(_authInvoker, conn, new ResultListener<AuthingConnection>() {
|
||||
public void requestCompleted (AuthingConnection conn) {
|
||||
_authq.append(conn);
|
||||
}
|
||||
@@ -547,7 +555,8 @@ public class ConnectionManager extends LoopingThread
|
||||
}
|
||||
|
||||
// and let the client manager know about our new connection
|
||||
_clmgr.connectionEstablished(rconn, conn.getAuthRequest(), conn.getAuthResponse());
|
||||
_clmgr.connectionEstablished(rconn, conn.getAuthName(), conn.getAuthRequest(),
|
||||
conn.getAuthResponse());
|
||||
|
||||
} catch (IOException ioe) {
|
||||
log.warning("Failure upgrading authing connection to running.", ioe);
|
||||
@@ -1187,6 +1196,7 @@ public class ConnectionManager extends LoopingThread
|
||||
* like the PeerManager may replace this authenticator with one that intercepts certain types
|
||||
* of authentication and then passes normal authentications through. */
|
||||
@Inject(optional=true) protected Authenticator _author = new DummyAuthenticator();
|
||||
protected List<ChainedAuthenticator> _authors = Lists.newArrayList();
|
||||
|
||||
protected int[] _ports, _datagramPorts;
|
||||
protected String _datagramHostname;
|
||||
|
||||
Reference in New Issue
Block a user