/**
*
*/
package org.limewire.lws.server;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.nio.entity.ConsumingNHttpEntity;
import org.apache.http.nio.entity.NStringEntity;
import org.apache.http.nio.protocol.NHttpResponseTrigger;
import org.apache.http.protocol.HttpContext;
import org.limewire.concurrent.ExecutorsHelper;
/**
* Instances of this class will receive HTTP requests and are responsible to
* doling them out to handlers. This is abstract so we can have test
* cases using some of the logic.
*/
public abstract class LWSDispatcherSupport implements LWSDispatcher {
private final static Log LOG = LogFactory.getLog(LWSDispatcherSupport.class);
private final Map<String, Handler> names2handlers = new HashMap<String, Handler>();
private LWSReceivesCommandsFromDispatcher commandReceiver;
private final Executor handlerExecutor = ExecutorsHelper.newProcessingQueue("lws-handlers");
/** Package protected for testing. */
public final static byte[] PING_BYTES = new byte[]{
(byte)0x89, (byte)0x50, (byte)0x4E, (byte)0x47, (byte)0x0D, (byte)0x0A, (byte)0x1A, (byte)0x0A, (byte)0x00,
(byte)0x00, (byte)0x00, (byte)0x0D, (byte)0x49, (byte)0x48, (byte)0x44, (byte)0x52, (byte)0x00, (byte)0x00,
(byte)0x00, (byte)0x01, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x01, (byte)0x08, (byte)0x06, (byte)0x00,
(byte)0x00, (byte)0x00, (byte)0x1F, (byte)0x15, (byte)0xC4, (byte)0x89, (byte)0x00, (byte)0x00, (byte)0x00,
(byte)0x01, (byte)0x73, (byte)0x52, (byte)0x47, (byte)0x42, (byte)0x00, (byte)0xAE, (byte)0xCE, (byte)0x1C,
(byte)0xE9, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x04, (byte)0x67, (byte)0x41, (byte)0x4D, (byte)0x41,
(byte)0x00, (byte)0x00, (byte)0xB1, (byte)0x8F, (byte)0x0B, (byte)0xFC, (byte)0x61, (byte)0x05, (byte)0x00,
(byte)0x00, (byte)0x00, (byte)0x20, (byte)0x63, (byte)0x48, (byte)0x52, (byte)0x4D, (byte)0x00, (byte)0x00,
(byte)0x7A, (byte)0x26, (byte)0x00, (byte)0x00, (byte)0x80, (byte)0x84, (byte)0x00, (byte)0x00, (byte)0xFA,
(byte)0x00, (byte)0x00, (byte)0x00, (byte)0x80, (byte)0xE8, (byte)0x00, (byte)0x00, (byte)0x75, (byte)0x30,
(byte)0x00, (byte)0x00, (byte)0xEA, (byte)0x60, (byte)0x00, (byte)0x00, (byte)0x3A, (byte)0x98, (byte)0x00,
(byte)0x00, (byte)0x17, (byte)0x70, (byte)0x9C, (byte)0xBA, (byte)0x51, (byte)0x3C, (byte)0x00, (byte)0x00,
(byte)0x00, (byte)0x18, (byte)0x74, (byte)0x45, (byte)0x58, (byte)0x74, (byte)0x53, (byte)0x6F, (byte)0x66,
(byte)0x74, (byte)0x77, (byte)0x61, (byte)0x72, (byte)0x65, (byte)0x00, (byte)0x50, (byte)0x61, (byte)0x69,
(byte)0x6E, (byte)0x74, (byte)0x2E, (byte)0x4E, (byte)0x45, (byte)0x54, (byte)0x20, (byte)0x76, (byte)0x33,
(byte)0x2E, (byte)0x31, (byte)0x30, (byte)0x72, (byte)0xB2, (byte)0x25, (byte)0x92, (byte)0x00, (byte)0x00,
(byte)0x00, (byte)0x0B, (byte)0x49, (byte)0x44, (byte)0x41, (byte)0x54, (byte)0x18, (byte)0x57, (byte)0x63,
(byte)0xF8, (byte)0x0F, (byte)0x04, (byte)0x00, (byte)0x09, (byte)0xFB, (byte)0x03, (byte)0xFD, (byte)0x2B,
(byte)0xD5, (byte)0x08, (byte)0x45, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x49, (byte)0x45,
(byte)0x4E, (byte)0x44, (byte)0xAE, (byte)0x42, (byte)0x60, (byte)0x82,
};
public LWSDispatcherSupport() {
Handler[] hs = createHandlers();
if (LOG.isDebugEnabled()) LOG.debug("creating " + hs.length + " handler(s)...");
for (Handler h : hs) {
this.names2handlers.put(h.name().toLowerCase(Locale.US), h);
if (LOG.isDebugEnabled()) LOG.debug(" - " + h.name());
}
}
// ------------------------------------------------------
// Abstract
// ------------------------------------------------------
/**
* Returns whether the web page has authenticated yet. This is used for
* determining whether to handle a PING request or not.
*/
protected abstract boolean isAuthenticated();
/*
* The abstraction is this. There are two subclasses of this class:
*
* - LWSMainDispatcher (for deployment)
* - RemoteServerImpl.DispatcherImpl (for testing)
*
* Both follow basically the pattern except the remote version is supposed
* be a mock Wicket server, so we have to send urls in a different way.
* In both cases below Here, StoreKey is the command, and the args are
* {"public" -> "JNGMLANSKC", "private" -> "LNUCOQSVOR"}
*
* LWSMainDispatcher takes CGI parameters in the normal way:
*
* - http://some.url/StoreKey?public=JNGMLANSKC&private=LNUCOQSVOR
*
* But RemoteServerImpl.DispatcherImpl takes them slightly differently:
*
* - http://some.url/StoreKey/public/JNGMLANSKC/private/LNUCOQSVOR
*
* In particular the correct call that we will see would look like this
*
* - store\app\pages\client\ClientCom\command\StoreKey\public\JNGMLANSKC\private\LNUCOQSVOR
*
* So we need to abstract out removing the command, and retrieving the arguments.
*/
/**
* Returns the command portion of a request or null if the parameter
* <code>command</code> is missing or the parameter <code>command</code>
* doesn't have an argument.
* <p>
* For example, in the case of {@link LWSDispatcherImpl} we would have the
* following:
*
* <pre>
* http://some.url/StoreKey?public=JNGMLANSKC&private=LNUCOQSVOR -> StoreKey
* http://some.url/?public=JNGMLANSKC&private=LNUCOQSVOR -> null
* </pre>
*
* In the case of {@link RemoteServerImpl.DispatcherImpl} we would have
*
* <pre>
* store\app\pages\client\ClientCom\command\StoreKey\public\JNGMLANSKC\private\LNUCOQSVOR -> StoreKey
* store\app\pages\client\ClientCom\command -> null
* store\app\pages\client\ClientCom\command\ -> null
* store\app\pages\client\ClientCom\ccccommand -> null
*
*/
protected abstract String getCommand(String request);
/**
* Returns the arguments portion of a request in the same order they appear.
*
* <p>
*
* For example, in the case of {@link LWSDispatcherImpl} we would have the
* following:
*
* <pre>
* http://some.url/StoreKey?public=JNGMLANSKC&private=LNUCOQSVOR -> {"public" -> "JNGMLANSKC", "private" -> "LNUCOQSVOR"}
* http://some.url/?public=JNGMLANSKC&private=LNUCOQSVOR -> {"public" -> "JNGMLANSKC", "private" -> "LNUCOQSVOR"}
* </pre>
*
* In the case of {@link RemoteServerImpl.DispatcherImpl} we would have
*
* <pre>
* store\app\pages\client\ClientCom\command\StoreKey\public\JNGMLANSKC\private\LNUCOQSVOR -> {"command" -> "StoreKey", "public" -> "JNGMLANSKC", "private" -> "LNUCOQSVOR"}
* store\app\pages\client\ClientCom\command -> {"command" -> null}
* store\app\pages\client\ClientCom\command\ -> {"command" -> null}
* store\app\pages\client\ClientCom\ccccommand -> {"command" -> null}
* </pre>
*
* @return arguments in the order they are placed in the URL
*/
protected abstract Map<String,String> getArgs(String request);
// ------------------------------------------------------
// Interface
// ------------------------------------------------------
public ConsumingNHttpEntity entityRequest(HttpEntityEnclosingRequest request,
HttpContext context) throws HttpException, IOException {
return null;
}
public final void handle(HttpRequest httpReq, final HttpResponse response,
final NHttpResponseTrigger trigger, HttpContext c) throws HttpException, IOException {
String request = httpReq.getRequestLine().getUri();
final String command = getCommand(request);
note("Have command {0} ", command);
//
// If the command is ping and we are authenticated, then send
// back the special PING response
//
if (isAuthenticated() && command.equals(Commands.PING)) {
note("Handling PING");
handlerExecutor.execute(new Runnable() {
public void run() {
response.setEntity(new ByteArrayEntity(PING_BYTES));
trigger.submitResponse(response);
}
});
notifyConnectionListeners(true);
return;
}
final Handler h = names2handlers.get(command.toLowerCase(Locale.US));
if (h == null) {
if (LOG.isErrorEnabled()) {
LOG.error("Couldn't create a handler for " + command);
}
String str = report(LWSDispatcherSupport.ErrorCodes.UNKNOWN_COMMAND);
response.setEntity(new NStringEntity(str));
trigger.submitResponse(response);
return;
}
if (LOG.isDebugEnabled())
LOG.debug("have handler: " + h.name());
final Map<String, String> args = getArgs(request);
note("Have args {0} ", args);
handlerExecutor.execute(new Runnable() {
public void run() {
h.handle(args, new StringCallback() {
public void process(String input) {
try {
note("Have response {0}",input);
response.setEntity(new NStringEntity(input));
trigger.submitResponse(response);
} catch (UnsupportedEncodingException e) {
trigger.handleException(e);
}
}
});
}
});
}
public final boolean addConnectionListener(LWSConnectionListener lis) {
return getCommandReceiver().addConnectionListener(lis);
}
public final boolean removeConnectionListener(LWSConnectionListener lis) {
return getCommandReceiver().removeConnectionListener(lis);
}
public final void notifyConnectionListeners(boolean isConnected) {
getCommandReceiver().setConnected(isConnected);
}
/**
* Override this to create the {@link Handler}s to use.
*
* @return the list of handlers that will take your requests.
*/
protected abstract Handler[] createHandlers();
/* (non-Javadoc)
* @see org.limewire.store.server.Dispatcher#setDispatchee(org.limewire.store.server.Dispatchee)
*/
public final void setCommandReceiver(LWSReceivesCommandsFromDispatcher commandReceiver) {
this.commandReceiver = commandReceiver;
}
/**
* Returns the {@link LWSReceivesCommandsFromDispatcher} instance.
*
* @return the {@link LWSReceivesCommandsFromDispatcher} instance
*/
final LWSReceivesCommandsFromDispatcher getCommandReceiver() {
return commandReceiver;
}
/**
* Create an instance of Handler from the top level name as well as trying a
* static inner class and calls its {@link Handler#handle()} method.
*/
public final void handle(String request, PrintStream out, StringCallback callback) {
final String req = getCommand(request);
if (isAuthenticated() && req.equals(Commands.PING)) {
note("Handling PING");
callback.process(new String(PING_BYTES));
notifyConnectionListeners(true);
return;
}
final Handler h = names2handlers.get(req.toLowerCase(Locale.US));
if (h == null) {
if (LOG.isErrorEnabled()) LOG.error("Couldn't create a handler for " + req);
callback.process(report(LWSDispatcherSupport.ErrorCodes.UNKNOWN_COMMAND));
return;
}
if (LOG.isDebugEnabled()) LOG.debug("have handler: " + h.name());
final Map<String, String> args = getArgs(request);
h.handle(args, callback);
}
final void note(String pattern, Object... os) {
if (LOG.isDebugEnabled()) LOG.info(MessageFormat.format(pattern, os));
}
/**
* Wraps the message <tt>error</tt> in the call back
* {@link Constants.ERROR_CALLBACK}. <br>
* Example: If the error message is <tt>"You stink!"</tt> the wrapped
* message would be <tt>error("You stink!")</tt>.
*
* @param error the error message
* @return the message <tt>error</tt> in the call back
*/
public static final String report(String error) {
return wrapCallback(LWSDispatcherSupport.Constants.ERROR_CALLBACK,
LWSServerUtil.wrapError(error));
}
/**
* Wraps the message <tt>msg</tt> using callback function
* <tt>callback</tt>. The message is surrounded by
* {@link Constants.CALLBACK_QUOTE}s and all quotes in the message,
* {@link Constants.CALLBACK_QUOTE}, are escaped.
*
* @param callback the function in which <tt>msg</tt> is wrapped, this can
* be <code>null</code>
* @param msg the message
* @return the message <tt>msg</tt> using callback function
* <tt>callback</tt>
*/
public static final String wrapCallback(final String callback, final String msg) {
if (LWSServerUtil.isEmpty(callback)) {
return msg;
} else {
char q = LWSDispatcherSupport.Constants.CALLBACK_QUOTE;
String s = LWSDispatcherSupport.Constants.CALLBACK_QUOTE_STRING;
return callback + "(" + q + (msg == null ? "" : msg.replace(s, "\\" + s)) + q + ")";
}
}
/**
* Something with a name.
*/
abstract static class HasName {
private final String name;
public HasName(final String name) {
this.name = name;
}
public HasName() {
String n = getClass().getName();
int ilast;
ilast = n.lastIndexOf(".");
if (ilast != -1)
n = n.substring(ilast + 1);
ilast = n.lastIndexOf("$");
if (ilast != -1)
n = n.substring(ilast + 1);
this.name = n;
}
public final String name() {
return name;
}
}
// ------------------------------------------------------------
// Handlers
// ------------------------------------------------------------
/**
* Handles commands.
*/
protected interface Handler {
/**
* Perform some operation on the incoming message and return the result.
*
* @param args CGI params
*/
void handle(Map<String, String> args, StringCallback callback);
/**
* Returns the unique name of this instance.
*
* @return the unique name of this instance
*/
String name();
}
/**
* Generic base class for {@link Handler}s.
*/
protected abstract static class AbstractHandler extends HasName implements Handler {
protected AbstractHandler(String name) {
super(name);
}
protected AbstractHandler() {
super();
}
protected String report(String msg) {
return LWSDispatcherSupport.report(msg);
}
}
/**
* A {@link Handler} requiring a callback specified by the
* parameter {@link Parameters#CALLBACK}.
*/
protected abstract class HandlerWithCallback extends AbstractHandler {
public final void handle(final Map<String, String> args, final StringCallback cb) {
final String callback = args.get(LWSDispatcherSupport.Parameters.CALLBACK);
if (callback == null) {
cb.process(report(LWSDispatcherSupport.ErrorCodes.MISSING_CALLBACK_PARAMETER));
return;
}
//
// We want to make sure to check if the result is an error. In which case
// we want to wrap it in the error callback, rather than the normal one
//
handleRest(args, new StringCallback() {
public void process(String res) {
String str;
if (LWSServerUtil.isError(res)) {
str = LWSDispatcherSupport.wrapCallback(Constants.ERROR_CALLBACK, res);
} else {
str = LWSDispatcherSupport.wrapCallback(callback, res);
}
cb.process(str);
}
});
}
/**
* Returns the result <b>IN PLAIN TEXT</b>. Override this to provide
* functionality after the {@link Parameters#CALLBACK} argument has been
* extracted. This method should <b>NOT</b> wrap the result in the
* callback, nor should it be called from any other method except this
* abstract class.
*
* <br/><br/>
*
* Instances of this class
* must not use {@link #report(String)}, and <b>must</b> only pass back
* error codes from {@link ErrorCodes}. To ensure that {@link #report(String)}
* is implemented to throw a {@link RuntimeException}.
*
* @param args original, untouched arguments
* @return result <b>IN PLAIN TEXT</b>
*/
protected abstract void handleRest(Map<String, String> args, StringCallback callback);
/**
* Overrides {@link AbstractHandler#report(String)} by simply wrapping
* the error with the error prefix as defined in {@link LWSServerUtil#wrapError(String)}
* so that we don't wrap it in a callback.
*/
@Override
protected final String report(String error) {
return LWSServerUtil.wrapError(error);
}
}
/**
* Something that can open an input stream on behalf of this component. The
* default implementation would be <code>new URL(url).openConnection()</code>
* but due to the <em>core's</em> connection manager we'll have to have a
* way of hooking into that without this component knowing about it.
*
* @see URLSocketOpenner
*/
interface OpensSocket {
/**
* Opens a connection based on the passed in URL <code>url</code>.
*
* @param host URL to open
* @return a connection based on the passed in URL <code>url</code>.
* @throws IOException if an IO error occurs
*/
Socket open(String host, int port) throws IOException;
}
/**
* Collection of all the commands we send.
*/
public interface Commands {
/**
* Sent from Code to Local with no parameters.
*/
String START_COM = "StartCom";
/**
* Sent from Local to Remote with parameters.
* <ul>
* <li>{@link LocalServer.Parameters#PUBLIC}</li>
* <li>{@link LocalServer.Parameters#PRIVATE}</li>
* </ul>
*/
String STORE_KEY = "StoreKey";
/**
* Sent from Code to Remote with parameters.
* <ul>
* <li>{@link LocalServer.Parameters#PRIVATE}</li>
* </ul>
*/
String GIVE_KEY = "GiveKey";
/**
* Send from Code to Local with no parameters.
*/
String DETATCH = "Detatch";
/**
* Sent from Code to Local with parameters.
* <ul>
* <li>{@link LocalServer.Parameters#PRIVATE}</li>
* </ul>
*/
String AUTHENTICATE = "Authenticate";
/**
* Sent from Code to Local no parameters for sending back the special ping response.
*/
String PING = "Ping";
/**
* Sent from Code to Local with parameters.
* <ul>
* <li>{@link LocalServer.Parameters#COMMAND}</li>
* </ul>
* for executing an actual command.
*/
String MSG = "Msg";
}
/**
* Parameter names.
*/
public interface Parameters {
/**
* Name of the callback function.
*/
String CALLBACK = "callback";
/**
* Private key.
*/
String PRIVATE = "private";
/**
* Public key.
*/
String PUBLIC = "public";
/**
* Shared key.
*/
String SHARED = "shared";
/**
* Name of the command to send to the {@link LWSReceivesCommandsFromDispatcher}.
*/
String COMMAND = "command";
/**
* Message to send to the <tt>ECHO</tt> command.
*/
String MSG = "msg";
/**
* Name of a URL.
*/
String URL = "url";
/**
* IP to store. This is sent in StoreKey from the local server to the
* remote server, but in the real system will be ignored.
*/
String IP = "ip";
}
/**
* Codes that are sent to the code (javascript) when an error occurs.
* NOTE: All errors have dots in them: https://www.limewire.org/fisheye/cru/LWCR-109/review#c3737
*/
public interface ErrorCodes {
/**
* Indicating an invalid public key.
*/
String INVALID_PUBLIC_KEY = "invalid.public.key";
/**
* Indicating an invalid private key.
*/
String INVALID_PRIVATE_KEY = "invalid.private.key";
/**
* Indicating an invalid shared key.
*/
String INVALID_SHARED_KEY = "invalid.shared.key";
/**
* Indicating an invalid public key or IP address.
*/
String INVALID_PUBLIC_KEY_OR_IP = "invalid.public.key.or.ip.address";
/**
* Indicating the code has not included a callback parameter.
*/
String MISSING_CALLBACK_PARAMETER = "missing.callback.parameter";
/**
* A command was not understood or did not have valid handler.
*/
String UNKNOWN_COMMAND = "unknown.command";
/**
* No private key has been generated yet.
*/
String UNITIALIZED_PRIVATE_KEY = "uninitialized.private.key";
/**
* No private key parameter was supplied.
*/
String MISSING_PRIVATE_KEY_PARAMETER = "missing.private.parameter";
/**
* No shared key has been generated yet.
*/
String UNITIALIZED_SHARED_KEY = "uninitialized.shared.key";
/**
* No shared key parameter was supplied.
*/
String MISSING_SHARED_KEY_PARAMETER = "missing.shared.parameter";
/**
* No public key parameter was supplied.
*/
String MISSING_PUBLIC_KEY_PARAMETER = "missing.public.parameter";
/**
* No command parameter was supplied to decide on a handler.
*/
String MISSING_COMMAND_PARAMETER = "missing.command.parameter";
/**
* No IP was given. In ths real system this will not be used.
*/
String MISSING_IP_PARAMETER = "missing.ip.parameter";
/**
* A parameter was missing.
*/
String MISSING_PARAMETER = "missing.parameter";
}
/**
* Responses sent back from servers.
*/
public interface Responses {
/**
* Success.
*/
String OK = "ok";
/**
* When there was a command sent to the local host, but no
* {@link LWSReceivesCommandsFromDispatcher} was set up to handle it.
*/
String NO_DISPATCHEE = "no.dispatcher";
/**
* When there was a {@link LWSReceivesCommandsFromDispatcher} to handle this command, but it
* didn't understand it.
*/
String UNKNOWN_COMMAND = "unknown.command";
}
/**
* A general place for constants.
*/
public interface Constants {
/**
* The length of the public and private keys generated.
*/
int KEY_LENGTH = 10; // TODO
/**
* Carriage return and line feed.
*/
String NEWLINE = "\r\n";
/**
* The quote used to surround callbacks. We need to escape this in the
* strings that we pass back to the callback.
*/
char CALLBACK_QUOTE = '\'';
/**
* The String version of {@link #CALLBACK_QUOTE}.
*/
String CALLBACK_QUOTE_STRING = String.valueOf(CALLBACK_QUOTE);
/**
* The callback in which error messages are wrapped.
*/
String ERROR_CALLBACK = "error";
/**
* The string that separates arguments in the {@link Parameters#MSG}
* argument when the command {@link Parameters#COMMAND} parameter is
* <tt>Msg</tt>. This is the urlencoded version of <tt>&</tt>,
* which is <tt>%26</tt>.
*/
String ARGUMENT_SEPARATOR = "%26";
}
}