add jetty-rhttp from codehaus to jetty-9

This commit is contained in:
Jesse McConnell 2012-08-29 11:49:16 -05:00
parent 553df4580a
commit d8e51cca72
54 changed files with 6596 additions and 0 deletions

33
jetty-rhttp/README.txt Normal file
View File

@ -0,0 +1,33 @@
Reverse HTTP
The HTTP server paradigm is a valuable abstraction for browsing and accessing data and applications in a RESTful fashion from thin clients or
other applications. However, when it comes to mobile devices, the server paradigm is often not available because those devices exist on
restricted networks that do not allow inbound connections. These devices (eg. phones, tablets, industrial controllers, etc.) often have
signficant content (eg. photos, video, music, contacts, etc.) and services (eg. GPS, phone, modem, camera, sound) that are worthwile to access
remotely and often the HTTP server model is very applicable.
The Jetty reverse HTTP module provides a gateway that efficiently allows HTTP connectivety to servers running in outbound-only networks. There are two key components:
The reverse HTTP connector is a jetty connector (like the HTTP, SSL, AJP connectors) that accepts HTTP requests for the Jetty server instance. However, the reverse HTTP connector does not accept inbound TCP/IP connections. Instead it makes an outbound HTTP connection to the reverse HTTP gateway and uses a long polling mechanism to efficiently and asynchronously fetch requests and send responses.
The reverse HTTP gateway is a jetty server that accepts inbound connections from one or more Reverse HTTP connectors and makes them available as normal HTTP targets.
To demonstrate this from a source release, first run a gateway instance:
cd jetty-reverse-http/reverse-http-gateway
mvn exec:java
In another window, you can run 3 test servers with reverse connectors with:
cd jetty-reverse-http/reverse-http-connector
mvn exec:java
The three servers are using context path ID's at the gateway (virtual host and cookie based mappings can also be done), so you can access the
three servers via the gateway at:
http://localhost:8080/gw/A
http://localhost:8080/gw/B
http://localhost:8080/gw/C

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.rhttp</groupId>
<artifactId>jetty-rhttp-project</artifactId>
<version>9.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>reverse-http-client</artifactId>
<packaging>jar</packaging>
<name>Jetty :: Reverse HTTP :: Client</name>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.0</version>
</dependency>
<dependency>
<groupId>net.jcip</groupId>
<artifactId>jcip-annotations</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,270 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* @version $Revision$ $Date$
*/
public abstract class AbstractClient extends AbstractLifeCycle implements RHTTPClient
{
private final Logger logger = Log.getLogger("org.mortbay.jetty.rhttp.client");
private final List<RHTTPListener> listeners = new CopyOnWriteArrayList<RHTTPListener>();
private final List<ClientListener> clientListeners = new CopyOnWriteArrayList<ClientListener>();
private final String targetId;
private volatile Status status = Status.DISCONNECTED;
public AbstractClient(String targetId)
{
this.targetId = targetId;
}
public String getGatewayURI()
{
return "http://"+getHost()+":"+getPort()+getPath();
}
public String getTargetId()
{
return targetId;
}
public Logger getLogger()
{
return logger;
}
public void addListener(RHTTPListener listener)
{
listeners.add(listener);
}
public void removeListener(RHTTPListener listener)
{
listeners.remove(listener);
}
public void addClientListener(ClientListener listener)
{
clientListeners.add(listener);
}
public void removeClientListener(ClientListener listener)
{
clientListeners.remove(listener);
}
protected void notifyRequests(List<RHTTPRequest> requests)
{
for (RHTTPRequest request : requests)
{
for (RHTTPListener listener : listeners)
{
try
{
listener.onRequest(request);
}
catch (Throwable x)
{
logger.warn("Listener " + listener + " threw", x);
try
{
deliver(newExceptionResponse(request.getId(), x));
}
catch (IOException xx)
{
logger.debug("Could not deliver exception response", xx);
}
}
}
}
}
protected RHTTPResponse newExceptionResponse(int requestId, Throwable x)
{
try
{
int statusCode = 500;
String statusMessage = "Internal Server Error";
Map<String, String> headers = new HashMap<String, String>();
byte[] body = x.toString().getBytes("UTF-8");
return new RHTTPResponse(requestId, statusCode, statusMessage, headers, body);
}
catch (UnsupportedEncodingException xx)
{
throw new AssertionError(xx);
}
}
protected void notifyConnectRequired()
{
for (ClientListener listener : clientListeners)
{
try
{
listener.connectRequired();
}
catch (Throwable x)
{
logger.warn("ClientListener " + listener + " threw", x);
}
}
}
protected void notifyConnectException()
{
for (ClientListener listener : clientListeners)
{
try
{
listener.connectException();
}
catch (Throwable x)
{
logger.warn("ClientListener " + listener + " threw", x);
}
}
}
protected void notifyConnectClosed()
{
for (ClientListener listener : clientListeners)
{
try
{
listener.connectClosed();
}
catch (Throwable xx)
{
logger.warn("ClientListener " + listener + " threw", xx);
}
}
}
protected void notifyDeliverException(RHTTPResponse response)
{
for (ClientListener listener : clientListeners)
{
try
{
listener.deliverException(response);
}
catch (Throwable x)
{
logger.warn("ClientListener " + listener + " threw", x);
}
}
}
protected String urlEncode(String value)
{
try
{
return URLEncoder.encode(value, "UTF-8");
}
catch (UnsupportedEncodingException x)
{
getLogger().debug("", x);
return null;
}
}
protected boolean isConnected()
{
return status == Status.CONNECTED;
}
protected boolean isDisconnecting()
{
return status == Status.DISCONNECTING;
}
protected boolean isDisconnected()
{
return status == Status.DISCONNECTED;
}
public void connect() throws IOException
{
if (isDisconnected())
status = Status.CONNECTING;
syncHandshake();
this.status = Status.CONNECTED;
asyncConnect();
}
public void disconnect() throws IOException
{
if (isConnected())
{
status = Status.DISCONNECTING;
try
{
syncDisconnect();
}
finally
{
status = Status.DISCONNECTED;
}
}
}
public void deliver(RHTTPResponse response) throws IOException
{
asyncDeliver(response);
}
protected abstract void syncHandshake() throws IOException;
protected abstract void asyncConnect();
protected abstract void syncDisconnect() throws IOException;
protected abstract void asyncDeliver(RHTTPResponse response);
protected void connectComplete(byte[] responseContent) throws IOException
{
List<RHTTPRequest> requests = RHTTPRequest.fromFrameBytes(responseContent);
getLogger().debug("Client {} connect returned from gateway, requests {}", getTargetId(), requests);
// Requests are arrived, reconnect while we process them
if (!isDisconnecting() && !isDisconnected())
asyncConnect();
notifyRequests(requests);
}
protected enum Status
{
CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED
}
}

View File

@ -0,0 +1,156 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.IOException;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.util.EntityUtils;
/**
* Implementation of {@link RHTTPClient} that uses Apache's HttpClient.
*
* @version $Revision$ $Date$
*/
public class ApacheClient extends AbstractClient
{
private final HttpClient httpClient;
private final String gatewayPath;
public ApacheClient(HttpClient httpClient, String gatewayPath, String targetId)
{
super(targetId);
this.httpClient = httpClient;
this.gatewayPath = gatewayPath;
}
public String getHost()
{
return ((HttpHost)httpClient.getParams().getParameter("http.default-host")).getHostName();
}
public int getPort()
{
return ((HttpHost)httpClient.getParams().getParameter("http.default-host")).getPort();
}
public String getPath()
{
return gatewayPath;
}
protected void syncHandshake() throws IOException
{
HttpPost handshake = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/handshake");
HttpResponse response = httpClient.execute(handshake);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (entity != null)
entity.consumeContent();
if (statusCode != HttpStatus.SC_OK)
throw new IOException("Handshake failed");
getLogger().debug("Client {} handshake returned from gateway", getTargetId(), null);
}
protected void asyncConnect()
{
new Thread()
{
@Override
public void run()
{
try
{
HttpPost connect = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/connect");
getLogger().debug("Client {} connect sent to gateway", getTargetId(), null);
HttpResponse response = httpClient.execute(connect);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
byte[] responseContent = EntityUtils.toByteArray(entity);
if (statusCode == HttpStatus.SC_OK)
connectComplete(responseContent);
else if (statusCode == HttpStatus.SC_UNAUTHORIZED)
notifyConnectRequired();
else
notifyConnectException();
}
catch (NoHttpResponseException x)
{
notifyConnectClosed();
}
catch (IOException x)
{
getLogger().debug("", x);
notifyConnectException();
}
}
}.start();
}
protected void syncDisconnect() throws IOException
{
HttpPost disconnect = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/disconnect");
HttpResponse response = httpClient.execute(disconnect);
int statusCode = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (entity != null)
entity.consumeContent();
if (statusCode != HttpStatus.SC_OK)
throw new IOException("Disconnect failed");
getLogger().debug("Client {} disconnect returned from gateway", getTargetId(), null);
}
protected void asyncDeliver(final RHTTPResponse response)
{
new Thread()
{
@Override
public void run()
{
try
{
HttpPost deliver = new HttpPost(gatewayPath + "/" + urlEncode(getTargetId()) + "/deliver");
deliver.setEntity(new ByteArrayEntity(response.getFrameBytes()));
getLogger().debug("Client {} deliver sent to gateway, response {}", getTargetId(), response);
HttpResponse httpResponse = httpClient.execute(deliver);
int statusCode = httpResponse.getStatusLine().getStatusCode();
HttpEntity entity = httpResponse.getEntity();
if (entity != null)
entity.consumeContent();
if (statusCode == HttpStatus.SC_UNAUTHORIZED)
notifyConnectRequired();
else if (statusCode != HttpStatus.SC_OK)
notifyDeliverException(response);
}
catch (IOException x)
{
getLogger().debug("", x);
notifyDeliverException(response);
}
}
}.start();
}
}

View File

@ -0,0 +1,67 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
/**
* A listener for network-related events happening on the gateway client.
*
* @version $Revision$ $Date$
*/
public interface ClientListener
{
/**
* Called when the client detects that the server requested a new connect.
*/
public void connectRequired();
/**
* Called when the client detects that the connection has been closed by the server.
*/
public void connectClosed();
/**
* Called when the client detects a generic exception while trying to connect to the server.
*/
public void connectException();
/**
* Called when the client detects a generic exception while tryint to deliver to the server.
* @param response the Response object that should have been sent to the server
*/
public void deliverException(RHTTPResponse response);
public static class Adapter implements ClientListener
{
public void connectRequired()
{
}
public void connectClosed()
{
}
public void connectException()
{
}
public void deliverException(RHTTPResponse response)
{
}
}
}

View File

@ -0,0 +1,306 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.io.EofException;
/**
* Implementation of {@link RHTTPClient} that uses Jetty's HttpClient.
*
* @version $Revision$ $Date$
*/
public class JettyClient extends AbstractClient
{
private final HttpClient httpClient;
private final Address gatewayAddress;
private final String gatewayPath;
public JettyClient(HttpClient httpClient, Address gatewayAddress, String gatewayPath, String targetId)
{
super(targetId);
this.httpClient = httpClient;
this.gatewayAddress = gatewayAddress;
this.gatewayPath = gatewayPath;
}
public JettyClient(HttpClient httpClient, String gatewayURI, String targetId)
{
super(targetId);
HttpURI uri = new HttpURI(gatewayURI);
this.httpClient = httpClient;
this.gatewayAddress = new Address(uri.getHost(),uri.getPort());
this.gatewayPath = uri.getPath();
}
public String getHost()
{
return gatewayAddress.getHost();
}
public int getPort()
{
return gatewayAddress.getPort();
}
public String getPath()
{
return gatewayPath;
}
@Override
protected void doStart() throws Exception
{
httpClient.start();
super.doStart();
}
@Override
protected void doStop() throws Exception
{
super.doStop();
httpClient.stop();
}
protected void syncHandshake() throws IOException
{
HandshakeExchange exchange = new HandshakeExchange();
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(gatewayAddress);
exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/handshake");
httpClient.send(exchange);
getLogger().debug("Client {} handshake sent to gateway", getTargetId(), null);
try
{
int exchangeStatus = exchange.waitForDone();
if (exchangeStatus != HttpExchange.STATUS_COMPLETED)
throw new IOException("Handshake failed");
if (exchange.getResponseStatus() != 200)
throw new IOException("Handshake failed");
getLogger().debug("Client {} handshake returned from gateway", getTargetId(), null);
}
catch (InterruptedException x)
{
Thread.currentThread().interrupt();
throw newIOException(x);
}
}
private IOException newIOException(Throwable x)
{
return (IOException)new IOException().initCause(x);
}
protected void asyncConnect()
{
try
{
ConnectExchange exchange = new ConnectExchange();
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(gatewayAddress);
exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/connect");
httpClient.send(exchange);
getLogger().debug("Client {} connect sent to gateway", getTargetId(), null);
}
catch (IOException x)
{
getLogger().debug("Could not send exchange", x);
throw new RuntimeException(x);
}
}
protected void syncDisconnect() throws IOException
{
DisconnectExchange exchange = new DisconnectExchange();
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(gatewayAddress);
exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/disconnect");
httpClient.send(exchange);
getLogger().debug("Client {} disconnect sent to gateway", getTargetId(), null);
try
{
int status = exchange.waitForDone();
if (status != HttpExchange.STATUS_COMPLETED)
throw new IOException("Disconnect failed");
if (exchange.getResponseStatus() != 200)
throw new IOException("Disconnect failed");
getLogger().debug("Client {} disconnect returned from gateway", getTargetId(), null);
}
catch (InterruptedException x)
{
Thread.currentThread().interrupt();
throw newIOException(x);
}
}
protected void asyncDeliver(RHTTPResponse response)
{
try
{
DeliverExchange exchange = new DeliverExchange(response);
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(gatewayAddress);
exchange.setURI(gatewayPath + "/" + urlEncode(getTargetId()) + "/deliver");
exchange.setRequestContent(new ByteArrayBuffer(response.getFrameBytes()));
httpClient.send(exchange);
getLogger().debug("Client {} deliver sent to gateway, response {}", getTargetId(), response);
}
catch (IOException x)
{
getLogger().debug("Could not send exchange", x);
throw new RuntimeException(x);
}
}
protected class HandshakeExchange extends ContentExchange
{
protected HandshakeExchange()
{
super(true);
}
@Override
protected void onConnectionFailed(Throwable x)
{
getLogger().warn(x.toString());
getLogger().debug(x);
}
}
protected class ConnectExchange extends ContentExchange
{
private final ByteArrayOutputStream content = new ByteArrayOutputStream();
protected ConnectExchange()
{
super(true);
}
@Override
protected void onResponseContent(Buffer buffer) throws IOException
{
buffer.writeTo(content);
}
@Override
protected void onResponseComplete()
{
int responseStatus = getResponseStatus();
if (responseStatus == 200)
{
try
{
connectComplete(content.toByteArray());
}
catch (IOException x)
{
onException(x);
}
}
else if (responseStatus == 401)
{
notifyConnectRequired();
}
else
{
notifyConnectException();
}
}
@Override
protected void onException(Throwable x)
{
getLogger().debug(x);
if (x instanceof EofException || x instanceof EOFException)
{
notifyConnectClosed();
}
else
{
notifyConnectException();
}
}
@Override
protected void onConnectionFailed(Throwable x)
{
getLogger().debug(x);
}
}
protected class DisconnectExchange extends ContentExchange
{
protected DisconnectExchange()
{
super(true);
}
}
protected class DeliverExchange extends ContentExchange
{
private final RHTTPResponse response;
protected DeliverExchange(RHTTPResponse response)
{
super(true);
this.response = response;
}
@Override
protected void onResponseComplete() throws IOException
{
int responseStatus = getResponseStatus();
if (responseStatus == 401)
{
notifyConnectRequired();
}
else if (responseStatus != 200)
{
notifyDeliverException(response);
}
}
@Override
protected void onException(Throwable x)
{
getLogger().debug(x);
notifyDeliverException(response);
}
@Override
protected void onConnectionFailed(Throwable x)
{
getLogger().debug(x);
}
}
}

View File

@ -0,0 +1,133 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.IOException;
/**
* <p><tt>RHTTPClient</tt> represent a client of the gateway server.</p>
* <p>A <tt>Client</tt> has a server side counterpart with which communicates
* using a comet protocol.<br /> The <tt>Client</tt>, its server-side
* counterpart and the comet protocol form the <em>Half-Object plus Protocol</em>
* pattern.</p>
* <p>A <tt>Client</tt> must first connect to the gateway server, to let the gateway
* server know its targetId, an identifier that uniquely distinguish this
* <tt>Client</tt> from other <tt>Client</tt>s.</p>
* <p>Once connected, the gateway server will use a comet procotol to notify the
* <tt>Client</tt> of server-side events, and the <tt>Client</tt> can send
* information to the gateway server to notify it of client-side events.</p>
* <p>Server-side event are notified to {@link RHTTPListener}s, while relevant
* network events are communicated to {@link ClientListener}s.</p>
*
* @version $Revision$ $Date$
*/
public interface RHTTPClient
{
/**
* @return The gateway uri, typically "http://gatewayhost:gatewayport/gatewaypath".
*/
public String getGatewayURI();
/**
* @return The gateway host
*/
public String getHost();
/**
* @return The gateway port
*/
public int getPort();
/**
* @return The gateway path
*/
public String getPath();
/**
* @return the targetId that uniquely identifies this client.
*/
public String getTargetId();
/**
* <p>Connects to the gateway server, establishing the long poll communication
* with the gateway server to be notified of server-side events.</p>
* <p>The connect is performed in two steps:
* <ul>
* <li>first, a connect message is sent to the gateway server; the gateway server
* will notice this is a first connect message and reply immediately with
* an empty response</li>
* <li>second, another connect message is sent to the gateway server which interprets
* it as a long poll request</li>
* </ul>
* The long poll request may return either because one or more server-side events
* happened, or because it expired. </p>
* <p>Any connect message after the first is treated as a long poll request.</p>
*
* @throws IOException if it is not possible to connect to the gateway server
* @see #disconnect()
*/
public void connect() throws IOException;
/**
* <p>Disconnects from the gateway server.</p>
* <p>Just after the disconnect request is processed by to the gateway server, it will
* return the currently outstanding long poll request.</p>
* <p>If this client is not connected, it does nothing</p>
*
* @throws IOException if it is not possible to contact the gateway server to disconnect
* @see #connect()
*/
public void disconnect() throws IOException;
/**
* <p>Sends a response to the gateway server.</p>
*
* @param response the response to send
* @throws IOException if it is not possible to contact the gateway server
*/
public void deliver(RHTTPResponse response) throws IOException;
/**
* <p>Adds the given listener to this client.</p>
* @param listener the listener to add
* @see #removeListener(RHTTPListener)
*/
public void addListener(RHTTPListener listener);
/**
* <p>Removes the given listener from this client.</p>
* @param listener the listener to remove
* @see #addListener(RHTTPListener)
*/
public void removeListener(RHTTPListener listener);
/**
* <p>Adds the given client listener to this client.</p>
* @param listener the client listener to add
* @see #removeClientListener(ClientListener)
*/
public void addClientListener(ClientListener listener);
/**
* <p>Removes the given client listener from this client.</p>
* @param listener the client listener to remove
* @see #addClientListener(ClientListener)
*/
public void removeClientListener(ClientListener listener);
}

View File

@ -0,0 +1,36 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
/**
* <p>Implementations of this class listen for requests arriving from the gateway server
* and notified by {@link RHTTPClient}.</p>
*
* @version $Revision$ $Date$
*/
public interface RHTTPListener
{
/**
* Callback method called by {@link RHTTPClient} to inform that the gateway server
* sent a request to the gateway client.
* @param request the request sent by the gateway server.
* @throws Exception allowed to be thrown by implementations
*/
public void onRequest(RHTTPRequest request) throws Exception;
}

View File

@ -0,0 +1,266 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.ByteArrayBuffer;
/**
* <p>Represents the external request information that is carried over the comet protocol.</p>
* <p>Instances of this class are converted into an opaque byte array of the form:</p>
* <pre>
* &lt;request-id&gt; SPACE &lt;request-length&gt; CRLF
* &lt;external-request&gt;
* </pre>
* <p>The byte array form is carried as body of a normal HTTP response returned by the gateway server
* to the gateway client.</p>
* @see RHTTPResponse
* @version $Revision$ $Date$
*/
public class RHTTPRequest
{
private static final String CRLF = "\r\n";
private static final byte[] CRLF_BYTES = CRLF.getBytes();
private final int id;
private final byte[] requestBytes;
private final byte[] frameBytes;
private volatile String method;
private volatile String uri;
private volatile Map<String, String> headers;
private volatile byte[] body;
public static List<RHTTPRequest> fromFrameBytes(byte[] bytes)
{
List<RHTTPRequest> result = new ArrayList<RHTTPRequest>();
int start = 0;
while (start < bytes.length)
{
// Scan until we find the space
int end = start;
while (bytes[end] != ' ') ++end;
int requestId = Integer.parseInt(new String(bytes, start, end - start));
start = end + 1;
// Scan until end of line
while (bytes[end] != '\n') ++end;
int length = Integer.parseInt(new String(bytes, start, end - start - 1));
start = end + 1;
byte[] requestBytes = new byte[length];
System.arraycopy(bytes, start, requestBytes, 0, length);
RHTTPRequest request = fromRequestBytes(requestId, requestBytes);
result.add(request);
start += length;
}
return result;
}
public static RHTTPRequest fromRequestBytes(int requestId, byte[] requestBytes)
{
return new RHTTPRequest(requestId, requestBytes);
}
public RHTTPRequest(int id, String method, String uri, Map<String, String> headers, byte[] body)
{
this.id = id;
this.method = method;
this.uri = uri;
this.headers = headers;
this.body = body;
this.requestBytes = toRequestBytes();
this.frameBytes = toFrameBytes(requestBytes);
}
private RHTTPRequest(int id, byte[] requestBytes)
{
this.id = id;
this.requestBytes = requestBytes;
this.frameBytes = toFrameBytes(requestBytes);
// Other fields are lazily initialized
}
private void initialize()
{
try
{
final ByteArrayOutputStream body = new ByteArrayOutputStream();
HttpParser parser = new HttpParser(new ByteArrayBuffer(requestBytes), new HttpParser.EventHandler()
{
@Override
public void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException
{
RHTTPRequest.this.method = method.toString("UTF-8");
RHTTPRequest.this.uri = uri.toString("UTF-8");
RHTTPRequest.this.headers = new LinkedHashMap<String, String>();
}
@Override
public void startResponse(Buffer httpVersion, int statusCode, Buffer statusMessage) throws IOException
{
}
@Override
public void parsedHeader(Buffer name, Buffer value) throws IOException
{
RHTTPRequest.this.headers.put(name.toString("UTF-8"), value.toString("UTF-8"));
}
@Override
public void content(Buffer content) throws IOException
{
content.writeTo(body);
}
});
parser.parse();
this.body = body.toByteArray();
}
catch (IOException x)
{
// Cannot happen: we're parsing from a byte[], not from an I/O stream
throw new AssertionError(x);
}
}
public int getId()
{
return id;
}
public byte[] getRequestBytes()
{
return requestBytes;
}
public byte[] getFrameBytes()
{
return frameBytes;
}
public String getMethod()
{
if (method == null)
initialize();
return method;
}
public String getURI()
{
if (uri == null)
initialize();
return uri;
}
public Map<String, String> getHeaders()
{
if (headers == null)
initialize();
return headers;
}
public byte[] getBody()
{
if (body == null)
initialize();
return body;
}
private byte[] toRequestBytes()
{
try
{
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(method.getBytes("UTF-8"));
bytes.write(' ');
bytes.write(uri.getBytes("UTF-8"));
bytes.write(' ');
bytes.write("HTTP/1.1".getBytes("UTF-8"));
bytes.write(CRLF_BYTES);
for (Map.Entry<String, String> entry : headers.entrySet())
{
bytes.write(entry.getKey().getBytes("UTF-8"));
bytes.write(':');
bytes.write(' ');
bytes.write(entry.getValue().getBytes("UTF-8"));
bytes.write(CRLF_BYTES);
}
bytes.write(CRLF_BYTES);
bytes.write(body);
bytes.close();
return bytes.toByteArray();
}
catch (IOException x)
{
throw new AssertionError(x);
}
}
private byte[] toFrameBytes(byte[] requestBytes)
{
try
{
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(String.valueOf(id).getBytes("UTF-8"));
bytes.write(' ');
bytes.write(String.valueOf(requestBytes.length).getBytes("UTF-8"));
bytes.write(CRLF_BYTES);
bytes.write(requestBytes);
bytes.close();
return bytes.toByteArray();
}
catch (IOException x)
{
throw new AssertionError(x);
}
}
@Override
public String toString()
{
// Use fields to avoid initialization
StringBuilder builder = new StringBuilder();
builder.append(id).append(" ");
builder.append(method).append(" ");
builder.append(uri).append(" ");
builder.append(requestBytes.length).append("/");
builder.append(frameBytes.length);
return builder.toString();
}
public String toLongString()
{
// Use getters to trigger initialization
StringBuilder builder = new StringBuilder();
builder.append(id).append(" ");
builder.append(getMethod()).append(" ");
builder.append(getURI()).append(CRLF);
for (Map.Entry<String, String> header : getHeaders().entrySet())
builder.append(header.getKey()).append(": ").append(header.getValue()).append(CRLF);
builder.append(getBody().length).append(" body bytes").append(CRLF);
return builder.toString();
}
}

View File

@ -0,0 +1,256 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.ByteArrayBuffer;
/**
* <p>Represents the resource provider response information that is carried over the comet protocol.</p>
* <p>Instances of this class are converted into an opaque byte array of the form:</p>
* <pre>
* &lt;request-id&gt; SPACE &lt;response-length&gt; CRLF
* &lt;resource-response&gt;
* </pre>
* <p>The byte array form is carried as body of a normal HTTP request made by the gateway client to
* the gateway server.</p>
* @see RHTTPRequest
* @version $Revision$ $Date$
*/
public class RHTTPResponse
{
private static final String CRLF = "\r\n";
private static final byte[] CRLF_BYTES = CRLF.getBytes();
private final int id;
private final byte[] responseBytes;
private final byte[] frameBytes;
private volatile int code;
private volatile String message;
private volatile Map<String, String> headers;
private volatile byte[] body;
public static RHTTPResponse fromFrameBytes(byte[] bytes)
{
int start = 0;
// Scan until we find the space
int end = start;
while (bytes[end] != ' ') ++end;
int responseId = Integer.parseInt(new String(bytes, start, end - start));
start = end + 1;
// Scan until end of line
while (bytes[end] != '\n') ++end;
int length = Integer.parseInt(new String(bytes, start, end - start - 1));
start = end + 1;
byte[] responseBytes = new byte[length];
System.arraycopy(bytes, start, responseBytes, 0, length);
return fromResponseBytes(responseId, responseBytes);
}
public static RHTTPResponse fromResponseBytes(int id, byte[] responseBytes)
{
return new RHTTPResponse(id, responseBytes);
}
public RHTTPResponse(int id, int code, String message, Map<String, String> headers, byte[] body)
{
this.id = id;
this.code = code;
this.message = message;
this.headers = headers;
this.body = body;
this.responseBytes = toResponseBytes();
this.frameBytes = toFrameBytes(responseBytes);
}
private RHTTPResponse(int id, byte[] responseBytes)
{
this.id = id;
this.responseBytes = responseBytes;
this.frameBytes = toFrameBytes(responseBytes);
// Other fields are lazily initialized
}
private void initialize()
{
try
{
final ByteArrayOutputStream body = new ByteArrayOutputStream();
HttpParser parser = new HttpParser(new ByteArrayBuffer(responseBytes), new HttpParser.EventHandler()
{
@Override
public void startRequest(Buffer method, Buffer uri, Buffer httpVersion) throws IOException
{
}
@Override
public void startResponse(Buffer httpVersion, int statusCode, Buffer statusMessage) throws IOException
{
RHTTPResponse.this.code = statusCode;
RHTTPResponse.this.message = statusMessage.toString("UTF-8");
RHTTPResponse.this.headers = new LinkedHashMap<String, String>();
}
@Override
public void parsedHeader(Buffer name, Buffer value) throws IOException
{
RHTTPResponse.this.headers.put(name.toString("UTF-8"), value.toString("UTF-8"));
}
@Override
public void content(Buffer content) throws IOException
{
content.writeTo(body);
}
});
parser.parse();
this.body = body.toByteArray();
}
catch (IOException x)
{
// Cannot happen: we're parsing from a byte[], not from an I/O stream
throw new AssertionError(x);
}
}
public int getId()
{
return id;
}
public byte[] getResponseBytes()
{
return responseBytes;
}
public byte[] getFrameBytes()
{
return frameBytes;
}
public int getStatusCode()
{
if (code == 0)
initialize();
return code;
}
public String getStatusMessage()
{
if (message == null)
initialize();
return message;
}
public Map<String, String> getHeaders()
{
if (headers == null)
initialize();
return headers;
}
public byte[] getBody()
{
if (body == null)
initialize();
return body;
}
private byte[] toResponseBytes()
{
try
{
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write("HTTP/1.1".getBytes("UTF-8"));
bytes.write(' ');
bytes.write(String.valueOf(code).getBytes("UTF-8"));
bytes.write(' ');
bytes.write(message.getBytes("UTF-8"));
bytes.write(CRLF_BYTES);
for (Map.Entry<String, String> entry : headers.entrySet())
{
bytes.write(entry.getKey().getBytes("UTF-8"));
bytes.write(':');
bytes.write(' ');
bytes.write(entry.getValue().getBytes("UTF-8"));
bytes.write(CRLF_BYTES);
}
bytes.write(CRLF_BYTES);
bytes.write(body);
bytes.close();
return bytes.toByteArray();
}
catch (IOException x)
{
throw new AssertionError(x);
}
}
private byte[] toFrameBytes(byte[] responseBytes)
{
try
{
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(String.valueOf(id).getBytes("UTF-8"));
bytes.write(' ');
bytes.write(String.valueOf(responseBytes.length).getBytes("UTF-8"));
bytes.write(CRLF_BYTES);
bytes.write(responseBytes);
return bytes.toByteArray();
}
catch (IOException x)
{
throw new AssertionError(x);
}
}
@Override
public String toString()
{
// Use fields to avoid initialization
StringBuilder builder = new StringBuilder();
builder.append(id).append(" ");
builder.append(code).append(" ");
builder.append(message).append(" ");
builder.append(responseBytes.length).append("/");
builder.append(frameBytes.length);
return builder.toString();
}
public String toLongString()
{
// Use getters to trigger initialization
StringBuilder builder = new StringBuilder();
builder.append(id).append(" ");
builder.append(getStatusCode()).append(" ");
builder.append(getStatusMessage()).append(CRLF);
for (Map.Entry<String, String> header : getHeaders().entrySet())
builder.append(header.getKey()).append(": ").append(header.getValue()).append(CRLF);
builder.append(getBody().length).append(" body bytes").append(CRLF);
return builder.toString();
}
}

View File

@ -0,0 +1,112 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.IOException;
import org.apache.http.client.HttpClient;
/**
* @version $Revision$ $Date$
*/
public class RetryingApacheClient extends ApacheClient
{
public RetryingApacheClient(HttpClient httpClient, String gatewayURI, String targetId)
{
super(httpClient, gatewayURI, targetId);
addClientListener(new RetryClientListener());
}
@Override
protected void syncHandshake() throws IOException
{
while (true)
{
try
{
super.syncHandshake();
break;
}
catch (IOException x)
{
getLogger().debug("Handshake failed, backing off and retrying");
try
{
Thread.sleep(1000);
}
catch (InterruptedException xx)
{
throw (IOException)new IOException().initCause(xx);
}
}
}
}
private class RetryClientListener implements ClientListener
{
public void connectRequired()
{
getLogger().debug("Connect requested by server");
try
{
connect();
}
catch (IOException x)
{
// The connect() method is retried, so if it fails, it's a hard failure
getLogger().debug("Connect failed after server required connect, giving up");
}
}
public void connectClosed()
{
connectException();
}
public void connectException()
{
getLogger().debug("Connect failed, backing off and retrying");
try
{
Thread.sleep(1000);
asyncConnect();
}
catch (InterruptedException x)
{
// Ignore and stop retrying
Thread.currentThread().interrupt();
}
}
public void deliverException(RHTTPResponse response)
{
getLogger().debug("Deliver failed, backing off and retrying");
try
{
Thread.sleep(1000);
asyncDeliver(response);
}
catch (InterruptedException x)
{
// Ignore and stop retrying
Thread.currentThread().interrupt();
}
}
}
}

View File

@ -0,0 +1,75 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.IOException;
import org.apache.http.HttpHost;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.eclipse.jetty.rhttp.client.ApacheClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StdErrLog;
/**
* @version $Revision$ $Date$
*/
public class ApacheClientTest extends ClientTest
{
{
((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
}
private ClientConnectionManager connectionManager;
protected RHTTPClient createClient(int port, String targetId) throws Exception
{
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), port));
connectionManager = new ThreadSafeClientConnManager(new BasicHttpParams(), schemeRegistry);
HttpParams httpParams = new BasicHttpParams();
httpParams.setParameter("http.default-host", new HttpHost("localhost", port));
DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, httpParams);
httpClient.setHttpRequestRetryHandler(new NoRetryHandler());
return new ApacheClient(httpClient, "", targetId);
}
protected void destroyClient(RHTTPClient client) throws Exception
{
connectionManager.shutdown();
}
private class NoRetryHandler implements HttpRequestRetryHandler
{
public boolean retryRequest(IOException x, int failedAttempts, HttpContext httpContext)
{
return false;
}
}
}

View File

@ -0,0 +1,299 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import junit.framework.TestCase;
import org.eclipse.jetty.rhttp.client.ClientListener;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.bio.SocketConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.util.log.Log;
/**
* @version $Revision$ $Date$
*/
public abstract class ClientTest extends TestCase
{
protected abstract RHTTPClient createClient(int port, String targetId) throws Exception;
protected abstract void destroyClient(RHTTPClient client) throws Exception;
public void testConnectNoServer() throws Exception
{
RHTTPClient client = createClient(8080, "test1");
try
{
client.connect();
fail();
}
catch (IOException x)
{
}
finally
{
destroyClient(client);
}
}
public void testServerExceptionOnHandshake() throws Exception
{
final CountDownLatch serverLatch = new CountDownLatch(1);
Server server = new Server();
Connector connector = new SelectChannelConnector();
server.addConnector(connector);
server.setHandler(new AbstractHandler()
{
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
{
request.setHandled(true);
if (target.endsWith("/handshake"))
{
serverLatch.countDown();
throw new TestException();
}
}
});
server.start();
try
{
RHTTPClient client = createClient(connector.getLocalPort(), "test2");
try
{
try
{
client.connect();
fail();
}
catch (IOException x)
{
}
assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
}
finally
{
destroyClient(client);
}
}
finally
{
server.stop();
}
}
public void testServerExceptionOnConnect() throws Exception
{
final CountDownLatch serverLatch = new CountDownLatch(1);
Server server = new Server();
Connector connector = new SelectChannelConnector();
server.addConnector(connector);
server.setHandler(new AbstractHandler()
{
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
{
request.setHandled(true);
if (target.endsWith("/connect"))
{
serverLatch.countDown();
throw new TestException();
}
}
});
server.start();
try
{
RHTTPClient client = createClient(connector.getLocalPort(), "test3");
try
{
final CountDownLatch connectLatch = new CountDownLatch(1);
client.addClientListener(new ClientListener.Adapter()
{
@Override
public void connectException()
{
connectLatch.countDown();
}
});
client.connect();
assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
assertTrue(connectLatch.await(1000, TimeUnit.MILLISECONDS));
}
finally
{
destroyClient(client);
}
}
finally
{
server.stop();
}
}
public void testServerExceptionOnDeliver() throws Exception
{
final CountDownLatch serverLatch = new CountDownLatch(1);
Server server = new Server();
Connector connector = new SelectChannelConnector();
server.addConnector(connector);
server.setHandler(new AbstractHandler()
{
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
{
request.setHandled(true);
if (target.endsWith("/connect"))
{
serverLatch.countDown();
try
{
// Simulate a long poll timeout
Thread.sleep(10000);
}
catch (InterruptedException x)
{
Thread.currentThread().interrupt();
}
}
else if (target.endsWith("/deliver"))
{
// Throw an exception on deliver
throw new TestException();
}
}
});
server.start();
try
{
RHTTPClient client = createClient(connector.getLocalPort(), "test4");
try
{
final CountDownLatch deliverLatch = new CountDownLatch(1);
client.addClientListener(new ClientListener.Adapter()
{
@Override
public void deliverException(RHTTPResponse response)
{
deliverLatch.countDown();
}
});
client.connect();
assertTrue(serverLatch.await(1000, TimeUnit.MILLISECONDS));
client.deliver(new RHTTPResponse(1, 200, "OK", new LinkedHashMap<String, String>(), new byte[0]));
assertTrue(deliverLatch.await(1000, TimeUnit.MILLISECONDS));
}
finally
{
destroyClient(client);
}
}
finally
{
server.stop();
}
}
public void testServerShutdownAfterConnect() throws Exception
{
final CountDownLatch connectLatch = new CountDownLatch(1);
final CountDownLatch stopLatch = new CountDownLatch(1);
Server server = new Server();
Connector connector = new SocketConnector();
server.addConnector(connector);
server.setHandler(new AbstractHandler()
{
public void handle(String target, Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
{
request.setHandled(true);
if (target.endsWith("/connect"))
{
connectLatch.countDown();
try
{
Thread.sleep(10000);
}
catch (InterruptedException e)
{
stopLatch.countDown();
}
}
}
});
server.start();
try
{
RHTTPClient client = createClient(connector.getLocalPort(), "test5");
try
{
final CountDownLatch serverLatch = new CountDownLatch(1);
client.addClientListener(new ClientListener.Adapter()
{
@Override
public void connectClosed()
{
serverLatch.countDown();
}
});
client.connect();
assertTrue(connectLatch.await(2000, TimeUnit.MILLISECONDS));
server.stop();
assertTrue(stopLatch.await(2000, TimeUnit.MILLISECONDS));
assertTrue(serverLatch.await(2000, TimeUnit.MILLISECONDS));
}
finally
{
destroyClient(client);
}
}
finally
{
server.stop();
}
}
public static class TestException extends NullPointerException
{
}
}

View File

@ -0,0 +1,52 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StdErrLog;
/**
* @version $Revision$ $Date$
*/
public class JettyClientTest extends ClientTest
{
{
((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
}
private HttpClient httpClient;
protected RHTTPClient createClient(int port, String targetId) throws Exception
{
((StdErrLog)Log.getLog()).setSource(true);
httpClient = new HttpClient();
httpClient.start();
return new JettyClient(httpClient, new Address("localhost", port), "", targetId);
}
protected void destroyClient(RHTTPClient client) throws Exception
{
httpClient.stop();
}
}

View File

@ -0,0 +1,85 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import junit.framework.TestCase;
/**
* @version $Revision$ $Date$
*/
public class RequestTest extends TestCase
{
public void testRequestConversions() throws Exception
{
int id = 1;
String method = "GET";
String uri = "/test";
Map<String, String> headers = new LinkedHashMap<String, String>();
headers.put("X", "X");
headers.put("Y", "Y");
headers.put("Z", "Z");
byte[] body = "BODY".getBytes("UTF-8");
headers.put("Content-Length", String.valueOf(body.length));
RHTTPRequest request1 = new RHTTPRequest(id, method, uri, headers, body);
byte[] requestBytes1 = request1.getRequestBytes();
RHTTPRequest request2 = RHTTPRequest.fromRequestBytes(id, requestBytes1);
assertEquals(id, request2.getId());
assertEquals(method, request2.getMethod());
assertEquals(uri, request2.getURI());
assertEquals(headers, request2.getHeaders());
assertTrue(Arrays.equals(request2.getBody(), body));
byte[] requestBytes2 = request2.getRequestBytes();
assertTrue(Arrays.equals(requestBytes1, requestBytes2));
}
public void testFrameConversions() throws Exception
{
int id = 1;
String method = "GET";
String uri = "/test";
Map<String, String> headers = new LinkedHashMap<String, String>();
headers.put("X", "X");
headers.put("Y", "Y");
headers.put("Z", "Z");
byte[] body = "BODY".getBytes("UTF-8");
headers.put("Content-Length", String.valueOf(body.length));
RHTTPRequest request1 = new RHTTPRequest(id, method, uri, headers, body);
byte[] frameBytes1 = request1.getFrameBytes();
List<RHTTPRequest> requests = RHTTPRequest.fromFrameBytes(frameBytes1);
assertNotNull(requests);
assertEquals(1, requests.size());
RHTTPRequest request2 = requests.get(0);
assertEquals(id, request2.getId());
assertEquals(method, request2.getMethod());
assertEquals(uri, request2.getURI());
assertEquals(headers, request2.getHeaders());
assertTrue(Arrays.equals(request2.getBody(), body));
byte[] frameBytes2 = request2.getFrameBytes();
assertTrue(Arrays.equals(frameBytes1, frameBytes2));
}
}

View File

@ -0,0 +1,85 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.client;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.StdErrLog;
import junit.framework.TestCase;
/**
* @version $Revision$ $Date$
*/
public class ResponseTest extends TestCase
{
{
((StdErrLog)Log.getLog()).setHideStacks(!Log.getLog().isDebugEnabled());
}
public void testResponseConversions() throws Exception
{
int id = 1;
int statusCode = 200;
String statusMessage = "OK";
Map<String, String> headers = new LinkedHashMap<String, String>();
headers.put("X", "X");
headers.put("Y", "Y");
headers.put("Z", "Z");
byte[] body = "BODY".getBytes("UTF-8");
RHTTPResponse response1 = new RHTTPResponse(id, statusCode, statusMessage, headers, body);
byte[] responseBytes1 = response1.getResponseBytes();
RHTTPResponse response2 = RHTTPResponse.fromResponseBytes(id, responseBytes1);
assertEquals(id, response2.getId());
assertEquals(statusCode, response2.getStatusCode());
assertEquals(statusMessage, response2.getStatusMessage());
assertEquals(headers, response2.getHeaders());
assertTrue(Arrays.equals(response2.getBody(), body));
byte[] responseBytes2 = response2.getResponseBytes();
assertTrue(Arrays.equals(responseBytes1, responseBytes2));
}
public void testFrameConversions() throws Exception
{
int id = 1;
int statusCode = 200;
String statusMessage = "OK";
Map<String, String> headers = new LinkedHashMap<String, String>();
headers.put("X", "X");
headers.put("Y", "Y");
headers.put("Z", "Z");
byte[] body = "BODY".getBytes("UTF-8");
RHTTPResponse response1 = new RHTTPResponse(id, statusCode, statusMessage, headers, body);
byte[] frameBytes1 = response1.getFrameBytes();
RHTTPResponse response2 = RHTTPResponse.fromFrameBytes(frameBytes1);
assertEquals(id, response2.getId());
assertEquals(statusCode, response2.getStatusCode());
assertEquals(response2.getStatusMessage(), statusMessage);
assertEquals(headers, response2.getHeaders());
assertTrue(Arrays.equals(response2.getBody(), body));
byte[] frameBytes2 = response2.getFrameBytes();
assertTrue(Arrays.equals(frameBytes1, frameBytes2));
}
}

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.rhttp</groupId>
<artifactId>jetty-rhttp-project</artifactId>
<version>9.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>reverse-http-connector</artifactId>
<packaging>jar</packaging>
<name>Jetty :: Reverse HTTP :: Connector</name>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<classpathScope>test</classpathScope>
<mainClass>org.eclipse.jetty.rhttp.connector.TestReverseServer</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>reverse-http-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>example-jetty-embedded</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<!-- =============================================================== -->
<!-- Configure the Jetty Reverse HTTP Connector -->
<!-- =============================================================== -->
<Configure id="Server" class="org.eclipse.jetty.server.Server">
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector">
<New class="org.eclipse.jetty.rhttp.client.JettyClient">
<Arg>
<New class="HttpClient">
</New>
</Arg>
<Arg>http://localhost:8888/</Arg>
<Arg>nodeA</Arg>
</New>
</New>
</Arg>
</Call>
</Configure>

View File

@ -0,0 +1,170 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.connector;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.eclipse.jetty.io.ByteArrayEndPoint;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPListener;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.server.AbstractConnector;
import org.eclipse.jetty.server.AbstractHttpConnection;
import org.eclipse.jetty.server.BlockingHttpConnection;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* An implementation of a Jetty connector that uses a {@link RHTTPClient} connected
* to a gateway server to receive requests, feed them to the Jetty server, and
* forward responses from the Jetty server to the gateway server.
*
* @version $Revision$ $Date$
*/
public class ReverseHTTPConnector extends AbstractConnector implements RHTTPListener
{
private static final Logger LOG = Log.getLogger(ReverseHTTPConnector.class);
private final BlockingQueue<RHTTPRequest> requests = new LinkedBlockingQueue<RHTTPRequest>();
private final RHTTPClient client;
public ReverseHTTPConnector(RHTTPClient client)
{
this.client = client;
super.setHost(client.getHost());
super.setPort(client.getPort());
}
@Override
public void setHost(String host)
{
throw new UnsupportedOperationException();
}
@Override
public void setPort(int port)
{
throw new UnsupportedOperationException();
}
@Override
protected void doStart() throws Exception
{
if (client instanceof LifeCycle)
((LifeCycle)client).start();
super.doStart();
client.connect();
}
@Override
protected void doStop() throws Exception
{
client.disconnect();
super.doStop();
if (client instanceof LifeCycle)
((LifeCycle)client).stop();
}
public void open()
{
client.addListener(this);
}
public void close()
{
client.removeListener(this);
}
public int getLocalPort()
{
return -1;
}
public Object getConnection()
{
return this;
}
@Override
protected void accept(int acceptorId) throws IOException, InterruptedException
{
RHTTPRequest request = requests.take();
IncomingRequest incomingRequest = new IncomingRequest(request);
getThreadPool().dispatch(incomingRequest);
}
@Override
public void persist(EndPoint endpoint) throws IOException
{
// Signals that the connection should not be closed
// Do nothing in this case, as we run from memory
}
public void onRequest(RHTTPRequest request) throws Exception
{
requests.add(request);
}
private class IncomingRequest implements Runnable
{
private final RHTTPRequest request;
private IncomingRequest(RHTTPRequest request)
{
this.request = request;
}
public void run()
{
byte[] requestBytes = request.getRequestBytes();
ByteArrayEndPoint endPoint = new ByteArrayEndPoint(requestBytes, 1024);
endPoint.setGrowOutput(true);
AbstractHttpConnection connection = new BlockingHttpConnection(ReverseHTTPConnector.this, endPoint, getServer());
connectionOpened(connection);
try
{
// Loop over the whole content, since handle() only
// reads up to the connection buffer's capacities
while (endPoint.getIn().length() > 0)
connection.handle();
byte[] responseBytes = endPoint.getOut().asArray();
RHTTPResponse response = RHTTPResponse.fromResponseBytes(request.getId(), responseBytes);
client.deliver(response);
}
catch (Exception x)
{
LOG.debug(x);
}
finally
{
connectionClosed(connection);
}
}
}
}

View File

@ -0,0 +1,187 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.connector;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import junit.framework.TestCase;
import org.eclipse.jetty.rhttp.client.ClientListener;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPListener;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
/**
* @version $Revision$ $Date$
*/
public class ReverseHTTPConnectorTest extends TestCase
{
public void testGatewayConnectorWithoutRequestBody() throws Exception
{
testGatewayConnector(false);
}
public void testGatewayConnectorWithRequestBody() throws Exception
{
testGatewayConnector(true);
}
private void testGatewayConnector(boolean withRequestBody) throws Exception
{
Server server = new Server();
final CountDownLatch handlerLatch = new CountDownLatch(1);
CountDownLatch clientLatch = new CountDownLatch(1);
AtomicReference<RHTTPResponse> responseRef = new AtomicReference<RHTTPResponse>();
ReverseHTTPConnector connector = new ReverseHTTPConnector(new TestClient(clientLatch, responseRef));
server.addConnector(connector);
final String method = "POST";
final String uri = "/test";
final byte[] requestBody = withRequestBody ? "REQUEST-BODY".getBytes("UTF-8") : new byte[0];
final int statusCode = HttpServletResponse.SC_CREATED;
final String headerName = "foo";
final String headerValue = "bar";
final byte[] responseBody = "RESPONSE-BODY".getBytes("UTF-8");
server.setHandler(new AbstractHandler()
{
public void handle(String pathInfo, org.eclipse.jetty.server.Request request, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException
{
assertEquals(method, httpRequest.getMethod());
assertEquals(uri, httpRequest.getRequestURI());
assertEquals(headerValue, httpRequest.getHeader(headerName));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream input = httpRequest.getInputStream();
int read;
while ((read = input.read()) >= 0)
baos.write(read);
baos.close();
assertTrue(Arrays.equals(requestBody, baos.toByteArray()));
httpResponse.setStatus(statusCode);
httpResponse.setHeader(headerName, headerValue);
OutputStream output = httpResponse.getOutputStream();
output.write(responseBody);
output.flush();
request.setHandled(true);
handlerLatch.countDown();
}
});
server.start();
HashMap<String, String> headers = new HashMap<String, String>();
headers.put("Host", "localhost");
headers.put(headerName, headerValue);
headers.put("Content-Length", String.valueOf(requestBody.length));
RHTTPRequest request = new RHTTPRequest(1, method, uri, headers, requestBody);
request = RHTTPRequest.fromRequestBytes(request.getId(), request.getRequestBytes());
connector.onRequest(request);
assertTrue(handlerLatch.await(1000, TimeUnit.MILLISECONDS));
assertTrue(clientLatch.await(1000, TimeUnit.MILLISECONDS));
RHTTPResponse response = responseRef.get();
assertEquals(request.getId(), response.getId());
assertEquals(statusCode, response.getStatusCode());
assertEquals(headerValue, response.getHeaders().get(headerName));
assertTrue(Arrays.equals(response.getBody(), responseBody));
}
private class TestClient implements RHTTPClient
{
private final CountDownLatch latch;
private final AtomicReference<RHTTPResponse> responseRef;
private TestClient(CountDownLatch latch, AtomicReference<RHTTPResponse> response)
{
this.latch = latch;
this.responseRef = response;
}
public String getTargetId()
{
return null;
}
public void connect() throws IOException
{
}
public void disconnect() throws IOException
{
}
public void deliver(RHTTPResponse response) throws IOException
{
responseRef.set(response);
latch.countDown();
}
public void addListener(RHTTPListener listener)
{
}
public void removeListener(RHTTPListener listener)
{
}
public void addClientListener(ClientListener listener)
{
}
public void removeClientListener(ClientListener listener)
{
}
public String getHost()
{
return null;
}
public int getPort()
{
return 0;
}
public String getGatewayURI()
{
// TODO Auto-generated method stub
return null;
}
public String getPath()
{
// TODO Auto-generated method stub
return null;
}
}
}

View File

@ -0,0 +1,58 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.connector;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.embedded.HelloHandler;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.connector.ReverseHTTPConnector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.log.Log;
/**
* A Test content server that uses a {@link ReverseHTTPConnector}.
* The main of this class starts 3 TestReversionServers with IDs A, B and C.
*/
public class TestReverseServer extends Server
{
TestReverseServer(String targetId)
{
setHandler(new HelloHandler("Hello "+targetId,"Hi from "+targetId));
HttpClient httpClient = new HttpClient();
RHTTPClient client = new JettyClient(httpClient,"http://localhost:8080/__rhttp",targetId);
ReverseHTTPConnector connector = new ReverseHTTPConnector(client);
addConnector(connector);
}
public static void main(String... args) throws Exception
{
Log.getLogger("org.mortbay.jetty.rhttp.client").setDebugEnabled(true);
TestReverseServer[] node = new TestReverseServer[] { new TestReverseServer("A"),new TestReverseServer("B"),new TestReverseServer("C") };
for (TestReverseServer s : node)
s.start();
for (TestReverseServer s : node)
s.join();
}
}

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.rhttp</groupId>
<artifactId>jetty-rhttp-project</artifactId>
<version>9.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>reverse-http-gateway</artifactId>
<packaging>jar</packaging>
<name>Jetty :: Reverse HTTP :: Gateway</name>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>org.mortbay.jetty.rhttp.gateway.Main</mainClass>
<arguments>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>reverse-http-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-continuation</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,92 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
/**
* <p>A <tt>ClientDelegate</tt> is the server-side counterpart of a gateway client.</p>
* <p>The gateway client, the comet protocol and the <tt>ClientDelegate</tt> form the
* <em>Half-Object plus Protocol</em> pattern that is used between the gateway server
* and the gateway client.</p>
* <p><tt>ClientDelegate</tt> offers a server-side API on top of the comet communication.<br />
* The API allows to enqueue server-side events to the gateway client, allows to
* flush them to the gateway client, and allows to close and dispose server-side
* resources when the gateway client disconnects.</p>
*
* @version $Revision$ $Date$
*/
public interface ClientDelegate
{
/**
* @return the targetId that uniquely identifies this client delegate.
*/
public String getTargetId();
/**
* <p>Enqueues the given request to the delivery queue so that it will be sent to the
* gateway client on the first flush occasion.</p>
* <p>Requests may fail to be queued, for example because the gateway client disconnected
* concurrently.</p>
*
* @param request the request to add to the delivery queue
* @return whether the request has been queued or not
* @see #process(HttpServletRequest)
*/
public boolean enqueue(RHTTPRequest request);
/**
* <p>Flushes the requests that have been {@link #enqueue(RHTTPRequest) enqueued}.</p>
* <p>If no requests have been enqueued, then this method may suspend the current request for
* the long poll timeout. <br />
* The request is suspended only if all these conditions holds true:
* <ul>
* <li>it is not the first time that this method is called for this client delegate</li>
* <li>no requests have been enqueued</li>
* <li>this client delegate is not closed</li>
* <li>the previous call to this method did not suspend the request</li>
* </ul>
* In all other cases, a response if sent to the gateway client, possibly containing no requests.
*
* @param httpRequest the HTTP request for the long poll request from the gateway client
* @return the list of requests to send to the gateway client, or null if no response should be sent
* to the gateway client
* @throws IOException in case of I/O exception while flushing content to the gateway client
* @see #enqueue(RHTTPRequest)
*/
public List<RHTTPRequest> process(HttpServletRequest httpRequest) throws IOException;
/**
* <p>Closes this client delegate, in response to a gateway client request to disconnect.</p>
* @see #isClosed()
*/
public void close();
/**
* @return whether this delegate client is closed
* @see #close()
*/
public boolean isClosed();
}

View File

@ -0,0 +1,224 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* The servlet that handles the communication with the gateway clients.
* @version $Revision$ $Date$
*/
public class ConnectorServlet extends HttpServlet
{
private final Logger logger = Log.getLogger(getClass().toString());
private final TargetIdRetriever targetIdRetriever = new StandardTargetIdRetriever();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ConcurrentMap<String, Future<?>> expirations = new ConcurrentHashMap<String, Future<?>>();
private final Gateway gateway;
private long clientTimeout=15000;
public ConnectorServlet(Gateway gateway)
{
this.gateway = gateway;
}
@Override
public void init() throws ServletException
{
String t = getInitParameter("clientTimeout");
if (t!=null && !"".equals(t))
clientTimeout=Long.parseLong(t);
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String targetId = targetIdRetriever.retrieveTargetId(request);
String uri = request.getRequestURI();
String path = uri.substring(request.getServletPath().length());
String[] segments = path.split("/");
if (segments.length < 3)
throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + uri);
String action = segments[2];
if ("handshake".equals(action))
serviceHandshake(targetId, request, response);
else if ("connect".equals(action))
serviceConnect(targetId, request, response);
else if ("deliver".equals(action))
serviceDeliver(targetId, request, response);
else if ("disconnect".equals(action))
serviceDisconnect(targetId, request, response);
else
throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + uri);
}
private void serviceHandshake(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
{
ClientDelegate client = gateway.getClientDelegate(targetId);
if (client != null)
throw new IOException("Client with targetId " + targetId + " is already connected");
client = gateway.newClientDelegate(targetId);
ClientDelegate existing = gateway.addClientDelegate(targetId, client);
if (existing != null)
throw new IOException("Client with targetId " + targetId + " is already connected");
flush(client, httpRequest, httpResponse);
}
private void flush(ClientDelegate client, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
{
List<RHTTPRequest> requests = client.process(httpRequest);
if (requests != null)
{
// Schedule before sending the requests, to avoid that the remote client
// reconnects before we have scheduled the expiration timeout.
if (!client.isClosed())
schedule(client);
ServletOutputStream output = httpResponse.getOutputStream();
for (RHTTPRequest request : requests)
output.write(request.getFrameBytes());
// I could count the framed bytes of all requests and set a Content-Length header,
// but the implementation of ServletOutputStream takes care of everything:
// if the request was HTTP/1.1, then flushing result in a chunked response, but the
// client know how to handle it; if the request was HTTP/1.0, then no chunking.
// To avoid chunking in HTTP/1.1 I must set the Content-Length header.
output.flush();
logger.debug("Delivered to device {} requests {} ", client.getTargetId(), requests);
}
}
private void schedule(ClientDelegate client)
{
Future<?> task = scheduler.schedule(new ClientExpirationTask(client), clientTimeout, TimeUnit.MILLISECONDS);
Future<?> existing = expirations.put(client.getTargetId(), task);
assert existing == null;
}
private void unschedule(String targetId)
{
Future<?> task = expirations.remove(targetId);
if (task != null)
task.cancel(false);
}
private void serviceConnect(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
{
unschedule(targetId);
ClientDelegate client = gateway.getClientDelegate(targetId);
if (client == null)
{
// Expired client tries to connect without handshake
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
flush(client, httpRequest, httpResponse);
if (client.isClosed())
gateway.removeClientDelegate(targetId);
}
private void expireConnect(ClientDelegate client, long time)
{
String targetId = client.getTargetId();
logger.info("Client with targetId {} missing, last seen {} ms ago, closing it", targetId, System.currentTimeMillis() - time);
client.close();
// If the client expired, means that it did not connect,
// so there no request to resume, and we cleanup here
// (while normally this cleanup is done in serviceConnect())
unschedule(targetId);
gateway.removeClientDelegate(targetId);
}
private void serviceDeliver(String targetId, HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException
{
if (gateway.getClientDelegate(targetId) == null)
{
// Expired client tries to deliver without handshake
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
byte[] body = Utils.read(httpRequest.getInputStream());
RHTTPResponse response = RHTTPResponse.fromFrameBytes(body);
ExternalRequest externalRequest = gateway.removeExternalRequest(response.getId());
if (externalRequest != null)
{
externalRequest.respond(response);
logger.debug("Deliver request from device {}, gateway request {}, response {}", new Object[] {targetId, externalRequest, response});
}
else
{
// We can arrive here for a race with the continuation expiration, which expired just before
// the gateway client responded with a valid response; log this case ignore it.
logger.debug("Deliver request from device {}, missing gateway request, response {}", targetId, response);
}
}
private void serviceDisconnect(String targetId, HttpServletRequest request, HttpServletResponse response)
{
// Do not remove the ClientDelegate from the gateway here,
// since closing the ClientDelegate will resume the connect request
// and we remove the ClientDelegate from the gateway there
ClientDelegate client = gateway.getClientDelegate(targetId);
if (client != null)
client.close();
}
private class ClientExpirationTask implements Runnable
{
private final long time = System.currentTimeMillis();
private final ClientDelegate client;
public ClientExpirationTask(ClientDelegate client)
{
this.client = client;
}
public void run()
{
expireConnect(client, time);
}
}
}

View File

@ -0,0 +1,54 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
/**
* <p><tt>ExternalRequest</tt> represent an external request made to the gateway server.</p>
* <p><tt>ExternalRequest</tt>s that arrive to the gateway server are suspended, waiting
* for a response from the corresponding gateway client.</p>
*
* @version $Revision$ $Date$
*/
public interface ExternalRequest
{
/**
* <p>Suspends this <tt>ExternalRequest</tt> waiting for a response from the gateway client.</p>
* @return true if the <tt>ExternalRequest</tt> has been suspended, false if the
* <tt>ExternalRequest</tt> has already been responded.
*/
public boolean suspend();
/**
* <p>Responds to the original external request with the response arrived from the gateway client.</p>
* @param response the response arrived from the gateway client
* @throws IOException if responding to the original external request fails
*/
public void respond(RHTTPResponse response) throws IOException;
/**
* @return the request to be sent to the gateway client
*/
public RHTTPRequest getRequest();
}

View File

@ -0,0 +1,88 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* The servlet that handles external requests.
*
* @version $Revision$ $Date$
*/
public class ExternalServlet extends HttpServlet
{
private final Logger logger = Log.getLogger(getClass().toString());
private final Gateway gateway;
private TargetIdRetriever targetIdRetriever;
public ExternalServlet(Gateway gateway, TargetIdRetriever targetIdRetriever)
{
this.gateway = gateway;
this.targetIdRetriever = targetIdRetriever;
}
public TargetIdRetriever getTargetIdRetriever()
{
return targetIdRetriever;
}
public void setTargetIdRetriever(TargetIdRetriever targetIdRetriever)
{
this.targetIdRetriever = targetIdRetriever;
}
@Override
protected void service(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException
{
logger.debug("External http request: {}", httpRequest.getRequestURL());
String targetId = targetIdRetriever.retrieveTargetId(httpRequest);
if (targetId == null)
throw new ServletException("Invalid request to " + getClass().getSimpleName() + ": " + httpRequest.getRequestURI());
ClientDelegate client = gateway.getClientDelegate(targetId);
if (client == null) throw new ServletException("Client with targetId " + targetId + " is not connected");
ExternalRequest externalRequest = gateway.newExternalRequest(httpRequest, httpResponse);
RHTTPRequest request = externalRequest.getRequest();
ExternalRequest existing = gateway.addExternalRequest(request.getId(), externalRequest);
assert existing == null;
logger.debug("External request {} for device {}", request, targetId);
boolean delivered = client.enqueue(request);
if (delivered)
{
externalRequest.suspend();
}
else
{
// TODO: improve this: we can temporarly queue this request elsewhere and wait for the client to reconnect ?
throw new ServletException("Could not enqueue request to client with targetId " + targetId);
}
}
}

View File

@ -0,0 +1,99 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* <p>Gateway instances are responsible of holding the state of the gateway server.</p>
* <p>The state is composed by:
* <ul>
* <li>{@link ExternalRequest external requests} that are suspended waiting for the response</li>
* <li>{@link ClientDelegate gateway clients} that are connected with the gateway server</li>
* </ul></p>
* <p>Instances of this class are created by the {@link GatewayServer}.</p>
*
* @version $Revision$ $Date$
*/
public interface Gateway
{
/**
* <p>Returns the {@link ClientDelegate} with the given targetId.<br />
* If there is no such ClientDelegate returns null.</p>
*
* @param targetId the targetId of the ClientDelegate to return
* @return the ClientDelegate associated with the given targetId
*/
public ClientDelegate getClientDelegate(String targetId);
/**
* <p>Creates and configures a new {@link ClientDelegate} with the given targetId.</p>
* @param targetId the targetId of the ClientDelegate to create
* @return a newly created ClientDelegate
* @see #addClientDelegate(String, ClientDelegate)
*/
public ClientDelegate newClientDelegate(String targetId);
/**
* <p>Maps the given ClientDelegate to the given targetId.</p>
* @param targetId the targetId of the given ClientDelegate
* @param client the ClientDelegate to map
* @return the previously existing ClientDelegate mapped to the same targetId
* @see #removeClientDelegate(String)
*/
public ClientDelegate addClientDelegate(String targetId, ClientDelegate client);
/**
* <p>Removes the {@link ClientDelegate} associated with the given targetId.</p>
* @param targetId the targetId of the ClientDelegate to remove
* @return the removed ClientDelegate, or null if no ClientDelegate was removed
* @see #addClientDelegate(String, ClientDelegate)
*/
public ClientDelegate removeClientDelegate(String targetId);
/**
* <p>Creates a new {@link ExternalRequest} from the given HTTP request and HTTP response.</p>
* @param httpRequest the HTTP request of the external request
* @param httpResponse the HTTP response of the external request
* @return a newly created ExternalRequest
* @throws IOException in case of failures creating the ExternalRequest
* @see #addExternalRequest(int, ExternalRequest)
*/
public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException;
/**
* Maps the given ExternalRequest with the given requestId into the gateway state.
* @param requestId the id of the ExternalRequest
* @param externalRequest the ExternalRequest to map
* @return the previously existing ExternalRequest mapped to the same requestId
* @see #removeExternalRequest(int)
*/
public ExternalRequest addExternalRequest(int requestId, ExternalRequest externalRequest);
/**
* Removes the ExternalRequest mapped to the given requestId from the gateway state.
* @param requestId the id of the ExternalRequest
* @return the removed ExternalRequest
* @see #addExternalRequest(int, ExternalRequest)
*/
public ExternalRequest removeExternalRequest(int requestId);
}

View File

@ -0,0 +1,228 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPListener;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* <p>This class combines a gateway server and a gateway client to obtain the functionality of a simple proxy server.</p>
* <p>This gateway proxy server starts on port 8080 and can be set as http proxy in browsers such as Firefox, and used
* to browse the internet.</p>
* <p>Its functionality is limited (for example, it only supports http, and not https).</p>
* @version $Revision$ $Date$
*/
public class GatewayProxyServer
{
private static final Logger logger = Log.getLogger(GatewayProxyServer.class.toString());
public static void main(String[] args) throws Exception
{
GatewayServer server = new GatewayServer();
Connector plainConnector = new SelectChannelConnector();
plainConnector.setPort(8080);
server.addConnector(plainConnector);
((StandardGateway)server.getGateway()).setExternalTimeout(180000);
((StandardGateway)server.getGateway()).setGatewayTimeout(20000);
server.setTargetIdRetriever(new ProxyTargetIdRetriever());
server.start();
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SOCKET);
httpClient.start();
RHTTPClient client = new JettyClient(httpClient, new Address("localhost", plainConnector.getPort()), server.getContext().getContextPath() + "/gw", "proxy");
client.addListener(new ProxyListener(httpClient, client));
client.connect();
Runtime.getRuntime().addShutdownHook(new Shutdown(server, httpClient, client));
logger.info("{} started", GatewayProxyServer.class.getSimpleName());
}
private static class Shutdown extends Thread
{
private final GatewayServer server;
private final HttpClient httpClient;
private final RHTTPClient client;
public Shutdown(GatewayServer server, HttpClient httpClient, RHTTPClient client)
{
this.server = server;
this.httpClient = httpClient;
this.client = client;
}
@Override
public void run()
{
try
{
client.disconnect();
httpClient.stop();
server.stop();
logger.info("{} stopped", GatewayProxyServer.class.getSimpleName());
}
catch (Exception x)
{
logger.debug("Exception while stopping " + GatewayProxyServer.class.getSimpleName(), x);
}
}
}
private static class ProxyListener implements RHTTPListener
{
private final HttpClient httpClient;
private final RHTTPClient client;
private ProxyListener(HttpClient httpClient, RHTTPClient client)
{
this.httpClient = httpClient;
this.client = client;
}
public void onRequest(RHTTPRequest request) throws Exception
{
ProxyExchange exchange = new ProxyExchange();
Address address = Address.from(request.getHeaders().get("Host"));
if (address.getPort() == 0) address = new Address(address.getHost(), 80);
exchange.setAddress(address);
exchange.setMethod(request.getMethod());
exchange.setURI(request.getURI());
for (Map.Entry<String, String> header : request.getHeaders().entrySet())
exchange.setRequestHeader(header.getKey(), header.getValue());
exchange.setRequestContent(new ByteArrayBuffer(request.getBody()));
int status = syncSend(exchange);
if (status == HttpExchange.STATUS_COMPLETED)
{
int statusCode = exchange.getResponseStatus();
String statusMessage = exchange.getResponseMessage();
Map<String, String> responseHeaders = exchange.getResponseHeaders();
byte[] responseBody = exchange.getResponseBody();
RHTTPResponse response = new RHTTPResponse(request.getId(), statusCode, statusMessage, responseHeaders, responseBody);
client.deliver(response);
}
else
{
int statusCode = HttpServletResponse.SC_SERVICE_UNAVAILABLE;
String statusMessage = "Gateway error";
HashMap<String, String> responseHeaders = new HashMap<String, String>();
responseHeaders.put("Connection", "close");
byte[] responseBody = new byte[0];
RHTTPResponse response = new RHTTPResponse(request.getId(), statusCode, statusMessage, responseHeaders, responseBody);
client.deliver(response);
}
}
private int syncSend(ProxyExchange exchange) throws Exception
{
long start = System.nanoTime();
httpClient.send(exchange);
int status = exchange.waitForDone();
long end = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(end - start);
long micros = TimeUnit.NANOSECONDS.toMicros(end - start - TimeUnit.MILLISECONDS.toNanos(millis));
logger.debug("Proxied request took {}.{} ms", millis, micros);
return status;
}
}
private static class ProxyExchange extends ContentExchange
{
private String responseMessage;
private Map<String, String> responseHeaders = new HashMap<String, String>();
private ByteArrayOutputStream responseBody = new ByteArrayOutputStream();
private ProxyExchange()
{
super(true);
}
public String getResponseMessage()
{
return responseMessage;
}
public Map<String, String> getResponseHeaders()
{
return responseHeaders;
}
public byte[] getResponseBody()
{
return responseBody.toByteArray();
}
@Override
protected void onResponseStatus(Buffer version, int code, Buffer message) throws IOException
{
super.onResponseStatus(version, code, message);
this.responseMessage = message.toString("UTF-8");
}
@Override
protected void onResponseHeader(Buffer nameBuffer, Buffer valueBuffer) throws IOException
{
super.onResponseHeader(nameBuffer, valueBuffer);
String name = nameBuffer.toString("UTF-8");
String value = valueBuffer.toString("UTF-8");
// Skip chunked header, since we read the whole body and will not re-chunk it
if (!name.equalsIgnoreCase("Transfer-Encoding") || !value.equalsIgnoreCase("chunked"))
responseHeaders.put(name, value);
}
@Override
protected void onResponseContent(Buffer buffer) throws IOException
{
responseBody.write(buffer.asArray());
super.onResponseContent(buffer);
}
}
public static class ProxyTargetIdRetriever implements TargetIdRetriever
{
public String retrieveTargetId(HttpServletRequest httpRequest)
{
return "proxy";
}
}
}

View File

@ -0,0 +1,171 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* <p>The gateway server is a server component that acts as intermediary between
* <em>external clients</em> which perform requests for resources, and the
* <em>resource providers</em>.</p>
* <p>The particularity of the gateway server is that the resource providers
* connect to the gateway using a comet protocol. <br />
* The comet procotol functionality is implemented by a gateway client. <br />
* This is quite different from a normal proxy server where it is the proxy that
* connects to the resource providers.</p>
* <p>Schematically, this is how the gateway server works:</p>
* <pre>
* External Client Gateway Server Gateway Client Resource Provider
* | |
* | &lt;-- comet req. 1 --- |
* | --- ext. req. 1 --&gt; | |
* | | --- comet res. 1 --&gt; |
* | | &lt;-- comet req. 2 --- |
* | | --- ext. req. 1 --&gt; |
* |
* | | &lt;-- ext. res. 1 --- |
* | | &lt;-- ext. res. 1 --- |
* | &lt;-- ext. res. 1 --- |
*
* | --- ext. req. 2 --&gt; |
* | | --- comet res. 2 --&gt; |
* . . .
* </pre>
* <p>The gateway server is made of two servlets:
* <ul>
* <li>the external servlet, that handles external requests</li>
* <li>the gateway servlet, that handles the communication with the gateway client</li>
* </ul>
* </p>
* <p>External requests are suspended using Jetty continuations until a response for
* that request arrives from the resource provider, or a
* {@link #getExternalTimeout() configurable timeout} expires. <br />
* Comet requests made by the gateway client also expires after a (different)
* {@link #getGatewayTimeout() configurable timeout}.</p>
* <p>External requests are packed into {@link RHTTPRequest} objects, converted into an
* opaque byte array and sent as the body of the comet reponse to the gateway
* {@link RHTTPClient}.</p>
* <p>The gateway client uses a notification mechanism to alert listeners interested
* in external requests that have been forwarded through the gateway. It is up to the
* listeners to connect to the resource provider however they like.</p>
* <p>When the gateway client receives a response from the resource provider, it packs
* the response into a {@link RHTTPResponse} object, converts it into an opaque byte array
* and sends it as the body of a normal HTTP request to the gateway server.</p>
* <p>It is possible to connect more than one gateway client to a gateway server; each
* gateway client is identified by a unique <em>targetId</em>. <br />
* External requests must specify a targetId that allows the gateway server to forward
* the requests to the specific gateway client; how the targetId is retrieved from an
* external request is handled by {@link TargetIdRetriever} implementations.</p>
*
* @version $Revision$ $Date$
*/
public class GatewayServer extends Server
{
public final static String DFT_EXT_PATH="/gw";
public final static String DFT_CONNECT_PATH="/__rhttp";
private final Logger logger = Log.getLogger(getClass().toString());
private final Gateway gateway;
private final ServletHolder externalServletHolder;
private final ServletHolder connectorServletHolder;
private final ServletContextHandler context;
public GatewayServer()
{
this("",DFT_EXT_PATH,DFT_CONNECT_PATH,new StandardTargetIdRetriever());
}
public GatewayServer(String contextPath, String externalServletPath,String gatewayServletPath, TargetIdRetriever targetIdRetriever)
{
HandlerCollection handlers = new HandlerCollection();
setHandler(handlers);
context = new ServletContextHandler(handlers, contextPath, ServletContextHandler.SESSIONS);
// Setup the gateway
gateway = createGateway();
// Setup external servlet
ExternalServlet externalServlet = new ExternalServlet(gateway, targetIdRetriever);
externalServletHolder = new ServletHolder(externalServlet);
context.addServlet(externalServletHolder, externalServletPath + "/*");
logger.debug("External servlet mapped to {}/*", externalServletPath);
// Setup gateway servlet
ConnectorServlet gatewayServlet = new ConnectorServlet(gateway);
connectorServletHolder = new ServletHolder(gatewayServlet);
connectorServletHolder.setInitParameter("clientTimeout", "15000");
context.addServlet(connectorServletHolder, gatewayServletPath + "/*");
logger.debug("Gateway servlet mapped to {}/*", gatewayServletPath);
}
/**
* Creates and configures a {@link Gateway} object.
* @return the newly created and configured Gateway object.
*/
protected Gateway createGateway()
{
StandardGateway gateway = new StandardGateway();
return gateway;
}
public ServletContextHandler getContext()
{
return context;
}
public Gateway getGateway()
{
return gateway;
}
public ServletHolder getExternalServlet()
{
return externalServletHolder;
}
public ServletHolder getConnectorServlet()
{
return connectorServletHolder;
}
public void setTargetIdRetriever(TargetIdRetriever retriever)
{
((ExternalServlet)externalServletHolder.getServletInstance()).setTargetIdRetriever(retriever);
}
public TargetIdRetriever getTargetIdRetriever()
{
return ((ExternalServlet)externalServletHolder.getServletInstance()).getTargetIdRetriever();
}
}

View File

@ -0,0 +1,54 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import javax.servlet.http.HttpServletRequest;
/**
* @version $Revision$ $Date$
*/
public class HostTargetIdRetriever implements TargetIdRetriever
{
private final String suffix;
public HostTargetIdRetriever(String suffix)
{
this.suffix = suffix;
}
public String retrieveTargetId(HttpServletRequest httpRequest)
{
String host = httpRequest.getHeader("Host");
if (host != null)
{
// Strip the port
int colon = host.indexOf(':');
if (colon > 0)
{
host = host.substring(0, colon);
}
if (suffix != null && host.endsWith(suffix))
{
return host.substring(0, host.length() - suffix.length());
}
}
return host;
}
}

View File

@ -0,0 +1,128 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletHolder;
/**
* <p>Main class that starts the gateway server.</p>
* <p>This class supports the following arguments:</p>
* <ul>
* <li>--port=&lt;port&gt; specifies the port on which the gateway server listens to, by default 8080</li>
* <li>--retriever=&lt;retriever&gt; specifies the
* {@link GatewayServer#setTargetIdRetriever(TargetIdRetriever) target id retriever}</li>
* <li>--resources=&lt;resources file path&gt; specifies the resource file path for the gateway</li>
* </ul>
* <p>Examples</p>
* <p> <tt>java --port=8080</tt> </p>
* <p> <tt>java --port=8080 --resources=/tmp/gateway-resources</tt> </p>
* <p> <tt>java --port=8080 --retriever=standard</tt> </p>
* <p> <tt>java --port=8080 --retriever=host,.rhttp.example.com</tt> </p>
* <p>The latter example specifies the {@link HostTargetIdRetriever} with a suffix of <tt>.rhttp.example.com</tt></p>
*
* @see GatewayServer
* @version $Revision$ $Date$
*/
public class Main
{
private static final String PORT_ARG = "port";
private static final String RESOURCES_ARG = "resources";
private static final String RETRIEVER_ARG = "retriever";
public static void main(String[] args) throws Exception
{
Map<String, Object> arguments = parse(args);
int port = 8080;
if (arguments.containsKey(PORT_ARG))
port = (Integer)arguments.get(PORT_ARG);
String resources = null;
if (arguments.containsKey(RESOURCES_ARG))
resources = (String)arguments.get(RESOURCES_ARG);
TargetIdRetriever retriever = null;
if (arguments.containsKey(RETRIEVER_ARG))
retriever = (TargetIdRetriever)arguments.get(RETRIEVER_ARG);
GatewayServer server = new GatewayServer();
Connector connector = new SelectChannelConnector();
connector.setPort(port);
server.addConnector(connector);
if (resources != null)
{
server.getContext().setResourceBase(resources);
ServletHolder resourcesServletHolder = server.getContext().addServlet(DefaultServlet.class,"__r/*");
resourcesServletHolder.setInitParameter("dirAllowed", "true");
}
if (retriever != null)
server.setTargetIdRetriever(retriever);
server.start();
}
private static Map<String, Object> parse(String[] args)
{
Map<String, Object> result = new HashMap<String, Object>();
Pattern pattern = Pattern.compile("--([^=]+)=(.+)");
for (String arg : args)
{
Matcher matcher = pattern.matcher(arg);
if (matcher.matches())
{
String argName = matcher.group(1);
if (PORT_ARG.equals(argName))
{
result.put(PORT_ARG, Integer.parseInt(matcher.group(2)));
}
else if (RESOURCES_ARG.equals(argName))
{
String argValue = matcher.group(2);
result.put(RESOURCES_ARG, argValue);
}
else if (RETRIEVER_ARG.equals(argName))
{
String argValue = matcher.group(2);
if (argValue.startsWith("host,"))
{
String[] typeAndSuffix = argValue.split(",");
if (typeAndSuffix.length != 2)
throw new IllegalArgumentException("Invalid option " + arg + ", must be of the form --" + RETRIEVER_ARG + "=host,suffix");
result.put(RETRIEVER_ARG, new HostTargetIdRetriever(typeAndSuffix[1]));
}
}
}
}
return result;
}
}

View File

@ -0,0 +1,172 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.continuation.Continuation;
import org.eclipse.jetty.continuation.ContinuationSupport;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* <p>Default implementation of {@link ClientDelegate}.</p>
*
* @version $Revision$ $Date$
*/
public class StandardClientDelegate implements ClientDelegate
{
private final Logger logger = Log.getLogger(getClass().toString());
private final Object lock = new Object();
private final List<RHTTPRequest> requests = new ArrayList<RHTTPRequest>();
private final String targetId;
private volatile boolean firstFlush = true;
private volatile long timeout;
private volatile boolean closed;
private Continuation continuation;
public StandardClientDelegate(String targetId)
{
this.targetId = targetId;
}
public String getTargetId()
{
return targetId;
}
public long getTimeout()
{
return timeout;
}
public void setTimeout(long timeout)
{
this.timeout = timeout;
}
public boolean enqueue(RHTTPRequest request)
{
if (isClosed())
return false;
synchronized (lock)
{
requests.add(request);
resume();
}
return true;
}
private void resume()
{
synchronized (lock)
{
// Continuation may be null in several cases:
// 1. there always is something to deliver so we never suspend
// 2. concurrent calls to add() and close()
// 3. concurrent close() with a long poll that expired
// 4. concurrent close() with a long poll that resumed
if (continuation != null)
{
continuation.resume();
// Null the continuation, as there is no point is resuming multiple times
continuation = null;
}
}
}
public List<RHTTPRequest> process(HttpServletRequest httpRequest) throws IOException
{
// We want to respond in the following cases:
// 1. It's the first time we process: the client will wait for a response before issuing another connect.
// 2. The client disconnected, so we want to return from this connect before it times out.
// 3. We've been woken up because there are responses to send.
// 4. The continuation was suspended but timed out.
// The timeout case is different from a non-first connect, in that we want to return
// a (most of the times empty) response and we do not want to wait again.
// The order of these if statements is important, as the continuation timed out only if
// the client is not closed and there are no responses to send
List<RHTTPRequest> result = Collections.emptyList();
if (firstFlush)
{
firstFlush = false;
logger.debug("Connect request (first) from device {}, delivering requests {}", targetId, result);
}
else
{
// Synchronization is crucial here, since we don't want to suspend if there is something to deliver
synchronized (lock)
{
int size = requests.size();
if (size > 0)
{
assert continuation == null;
result = new ArrayList<RHTTPRequest>(size);
result.addAll(requests);
requests.clear();
logger.debug("Connect request (resumed) from device {}, delivering requests {}", targetId, result);
}
else
{
if (continuation != null)
{
continuation = null;
logger.debug("Connect request (expired) from device {}, delivering requests {}", targetId, result);
}
else
{
if (isClosed())
{
logger.debug("Connect request (closed) from device {}, delivering requests {}", targetId, result);
}
else
{
// Here we need to suspend
continuation = ContinuationSupport.getContinuation(httpRequest);
continuation.setTimeout(getTimeout());
continuation.suspend();
result = null;
logger.debug("Connect request (suspended) from device {}", targetId);
}
}
}
}
}
return result;
}
public void close()
{
closed = true;
resume();
}
public boolean isClosed()
{
return closed;
}
}

View File

@ -0,0 +1,189 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import java.util.Map;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.continuation.Continuation;
import org.eclipse.jetty.continuation.ContinuationListener;
import org.eclipse.jetty.continuation.ContinuationSupport;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Default implementation of {@link ExternalRequest}.
*
* @version $Revision$ $Date$
*/
public class StandardExternalRequest implements ExternalRequest
{
private final Logger logger = Log.getLogger(getClass().toString());
private final RHTTPRequest request;
private final HttpServletRequest httpRequest;
private final HttpServletResponse httpResponse;
private final Gateway gateway;
private final Object lock = new Object();
private volatile long timeout;
private Continuation continuation;
private boolean responded;
public StandardExternalRequest(RHTTPRequest request, HttpServletRequest httpRequest, HttpServletResponse httpResponse, Gateway gateway)
{
this.request = request;
this.httpRequest = httpRequest;
this.httpResponse = httpResponse;
this.gateway = gateway;
}
public long getTimeout()
{
return timeout;
}
public void setTimeout(long timeout)
{
this.timeout = timeout;
}
public boolean suspend()
{
synchronized (lock)
{
// We suspend only if we have no responded yet
if (!responded)
{
assert continuation == null;
continuation = ContinuationSupport.getContinuation(httpRequest);
continuation.setTimeout(getTimeout());
continuation.addContinuationListener(new TimeoutListener());
continuation.suspend(httpResponse);
logger.debug("Request {} suspended", getRequest());
}
else
{
logger.debug("Request {} already responded", getRequest());
}
return !responded;
}
}
public void respond(RHTTPResponse response) throws IOException
{
responseCompleted(response);
}
private void responseCompleted(RHTTPResponse response) throws IOException
{
synchronized (lock)
{
// Could be that we complete exactly when the response is being expired
if (!responded)
{
httpResponse.setStatus(response.getStatusCode());
for (Map.Entry<String, String> header : response.getHeaders().entrySet())
httpResponse.setHeader(header.getKey(), header.getValue());
ServletOutputStream output = httpResponse.getOutputStream();
output.write(response.getBody());
output.flush();
// It may happen that the continuation is null,
// because the response arrived before we had the chance to suspend
if (continuation != null)
{
continuation.complete();
continuation = null;
}
// Mark as responded, so we know we don't have to suspend
// or respond with an expired response
responded = true;
if (logger.isDebugEnabled())
{
String eol = System.getProperty("line.separator");
logger.debug("Request {} responded {}{}{}{}{}", new Object[]{request, response, eol, request.toLongString(), eol, response.toLongString()});
}
}
}
}
private void responseExpired() throws IOException
{
synchronized (lock)
{
// Could be that we expired exactly when the response is being completed
if (!responded)
{
httpResponse.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT, "Gateway Time-out");
continuation.complete();
continuation = null;
// Mark as responded, so we know we don't have to respond with a completed response
responded = true;
logger.debug("Request {} expired", getRequest());
}
}
}
public RHTTPRequest getRequest()
{
return request;
}
@Override
public String toString()
{
return request.toString();
}
private class TimeoutListener implements ContinuationListener
{
public void onComplete(Continuation continuation)
{
}
public void onTimeout(Continuation continuation)
{
ExternalRequest externalRequest = gateway.removeExternalRequest(getRequest().getId());
// The gateway request can be null for a race with delivery
if (externalRequest != null)
{
try
{
responseExpired();
}
catch (Exception x)
{
logger.warn("Request " + getRequest() + " expired but failed", x);
}
}
}
}
}

View File

@ -0,0 +1,131 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Default implementation of {@link Gateway}.
*
* @version $Revision$ $Date$
*/
public class StandardGateway implements Gateway
{
private final Logger logger = Log.getLogger(getClass().toString());
private final ConcurrentMap<String, ClientDelegate> clients = new ConcurrentHashMap<String, ClientDelegate>();
private final ConcurrentMap<Integer, ExternalRequest> requests = new ConcurrentHashMap<Integer, ExternalRequest>();
private final AtomicInteger requestIds = new AtomicInteger();
private volatile long gatewayTimeout=20000;
private volatile long externalTimeout=60000;
public long getGatewayTimeout()
{
return gatewayTimeout;
}
public void setGatewayTimeout(long timeout)
{
this.gatewayTimeout = timeout;
}
public long getExternalTimeout()
{
return externalTimeout;
}
public void setExternalTimeout(long externalTimeout)
{
this.externalTimeout = externalTimeout;
}
public ClientDelegate getClientDelegate(String targetId)
{
return clients.get(targetId);
}
public ClientDelegate newClientDelegate(String targetId)
{
StandardClientDelegate client = new StandardClientDelegate(targetId);
client.setTimeout(getGatewayTimeout());
return client;
}
public ClientDelegate addClientDelegate(String targetId, ClientDelegate client)
{
return clients.putIfAbsent(targetId, client);
}
public ClientDelegate removeClientDelegate(String targetId)
{
return clients.remove(targetId);
}
public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
{
int requestId = requestIds.incrementAndGet();
RHTTPRequest request = convertHttpRequest(requestId, httpRequest);
StandardExternalRequest gatewayRequest = new StandardExternalRequest(request, httpRequest, httpResponse, this);
gatewayRequest.setTimeout(getExternalTimeout());
return gatewayRequest;
}
protected RHTTPRequest convertHttpRequest(int requestId, HttpServletRequest httpRequest) throws IOException
{
Map<String, String> headers = new HashMap<String, String>();
for (Enumeration headerNames = httpRequest.getHeaderNames(); headerNames.hasMoreElements();)
{
String name = (String)headerNames.nextElement();
// TODO: improve by supporting getHeaders(name)
String value = httpRequest.getHeader(name);
headers.put(name, value);
}
byte[] body = Utils.read(httpRequest.getInputStream());
return new RHTTPRequest(requestId, httpRequest.getMethod(), httpRequest.getRequestURI(), headers, body);
}
public ExternalRequest addExternalRequest(int requestId, ExternalRequest externalRequest)
{
ExternalRequest existing = requests.putIfAbsent(requestId, externalRequest);
if (existing == null)
logger.debug("Added external request {}/{} - {}", new Object[]{requestId, requests.size(), externalRequest});
return existing;
}
public ExternalRequest removeExternalRequest(int requestId)
{
ExternalRequest externalRequest = requests.remove(requestId);
if (externalRequest != null)
logger.debug("Removed external request {}/{} - {}", new Object[]{requestId, requests.size(), externalRequest});
return externalRequest;
}
}

View File

@ -0,0 +1,40 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import javax.servlet.http.HttpServletRequest;
/**
* <p>This implementation retrieves the targetId from the request URI following this pattern:</p>
* <pre>
* /contextPath/servletPath/&lt;targetId&gt;/other/paths
* </pre>
* @version $Revision$ $Date$
*/
public class StandardTargetIdRetriever implements TargetIdRetriever
{
public String retrieveTargetId(HttpServletRequest httpRequest)
{
String uri = httpRequest.getRequestURI();
String path = uri.substring(httpRequest.getServletPath().length());
String[] segments = path.split("/");
if (segments.length < 2) return null;
return segments[1];
}
}

View File

@ -0,0 +1,40 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import javax.servlet.http.HttpServletRequest;
/**
* <p>Implementations should retrieve a <em>targetId</em> from an external request.</p>
* <p>Implementations of this class may return a fixed value, or inspect the request
* looking for URL patterns (e.g. "/&lt;targetId&gt;/resource.jsp"), or looking for request
* parameters (e.g. "/resource.jsp?targetId=&lt;targetId&gt;), or looking for virtual host
* naming patterns (e.g. "http://&lt;targetId&gt;.host.com/resource.jsp"), etc.</p>
*
* @version $Revision$ $Date$
*/
public interface TargetIdRetriever
{
/**
* Extracts and returns the targetId.
* @param httpRequest the external request from where the targetId could be extracted
* @return the extracted targetId
*/
public String retrieveTargetId(HttpServletRequest httpRequest);
}

View File

@ -0,0 +1,40 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @version $Revision$ $Date$
*/
class Utils
{
static byte[] read(InputStream input) throws IOException
{
ByteArrayOutputStream body = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read;
while ((read = input.read(buffer)) >= 0)
body.write(buffer, 0, read);
body.close();
return body.toByteArray();
}
}

View File

@ -0,0 +1,115 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import junit.framework.TestCase;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.rhttp.client.ClientListener;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class ClientTimeoutTest extends TestCase
{
public void testClientTimeout() throws Exception
{
GatewayServer server = new GatewayServer();
Connector connector = new SelectChannelConnector();
server.addConnector(connector);
final long clientTimeout = 2000L;
server.getConnectorServlet().setInitParameter("clientTimeout",""+clientTimeout);
final long gatewayTimeout = 4000L;
((StandardGateway)server.getGateway()).setGatewayTimeout(gatewayTimeout);
server.start();
try
{
Address address = new Address("localhost", connector.getLocalPort());
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
try
{
String targetId = "1";
final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId)
{
private final AtomicInteger connects = new AtomicInteger();
@Override
protected void asyncConnect()
{
if (connects.incrementAndGet() == 2)
{
try
{
// Wait here instead of connecting, so that the client expires on the server
Thread.sleep(clientTimeout * 2);
}
catch (InterruptedException x)
{
throw new RuntimeException(x);
}
}
super.asyncConnect();
}
};
final CountDownLatch connectLatch = new CountDownLatch(1);
client.addClientListener(new ClientListener.Adapter()
{
@Override
public void connectRequired()
{
connectLatch.countDown();
}
});
client.connect();
try
{
assertTrue(connectLatch.await(gatewayTimeout + clientTimeout * 3, TimeUnit.MILLISECONDS));
}
finally
{
client.disconnect();
}
}
finally
{
httpClient.stop();
}
}
finally
{
server.stop();
}
}
}

View File

@ -0,0 +1,93 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import junit.framework.TestCase;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class DisconnectClientTest extends TestCase
{
public void testDifferentClientDisconnects() throws Exception
{
GatewayServer server = new GatewayServer();
Connector connector = new SelectChannelConnector();
server.addConnector(connector);
server.start();
try
{
Address address = new Address("localhost", connector.getLocalPort());
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
try
{
final CountDownLatch latch = new CountDownLatch(1);
String targetId = "1";
final RHTTPClient client1 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId)
{
@Override
protected void connectComplete(byte[] responseContent) throws IOException
{
// If the other client can disconnect this one, this method is called soon after it disconnected
latch.countDown();
super.connectComplete(responseContent);
}
};
client1.connect();
try
{
final RHTTPClient client2 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
// Disconnect client 2, this should not disconnect client1
client2.disconnect();
// We want the await() to expire, it means it has not disconnected
assertFalse(latch.await(1000, TimeUnit.MILLISECONDS));
}
finally
{
client1.disconnect();
}
}
finally
{
httpClient.stop();
}
}
finally
{
server.stop();
}
}
}

View File

@ -0,0 +1,84 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import junit.framework.TestCase;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class DuplicateClientTest extends TestCase
{
public void testDuplicateClient() throws Exception
{
GatewayServer server = new GatewayServer();
Connector connector = new SelectChannelConnector();
server.addConnector(connector);
server.start();
try
{
Address address = new Address("localhost", connector.getLocalPort());
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
try
{
String targetId = "1";
final RHTTPClient client1 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
client1.connect();
try
{
final RHTTPClient client2 = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
try
{
client2.connect();
fail();
}
catch (IOException x)
{
}
}
finally
{
client1.disconnect();
}
}
finally
{
httpClient.stop();
}
}
finally
{
server.stop();
}
}
}

View File

@ -0,0 +1,186 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import junit.framework.TestCase;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPListener;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.rhttp.gateway.ExternalRequest;
import org.eclipse.jetty.rhttp.gateway.Gateway;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class ExternalRequestNotSuspendedTest extends TestCase
{
public void testExternalRequestNotSuspended() throws Exception
{
final CountDownLatch respondLatch = new CountDownLatch(1);
final CountDownLatch suspendLatch = new CountDownLatch(1);
final AtomicBoolean suspended = new AtomicBoolean(true);
GatewayServer server = new GatewayServer()
{
@Override
protected Gateway createGateway()
{
StandardGateway gateway = new StandardGateway()
{
@Override
public ExternalRequest newExternalRequest(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException
{
return new SlowToSuspendExternalRequest(super.newExternalRequest(httpRequest, httpResponse), respondLatch, suspendLatch, suspended);
}
};
return gateway;
}
};
SelectChannelConnector connector = new SelectChannelConnector();
server.addConnector(connector);
server.start();
try
{
Address address = new Address("localhost", connector.getLocalPort());
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
try
{
String targetId = "1";
final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
final AtomicReference<Exception> exception = new AtomicReference<Exception>();
client.addListener(new RHTTPListener()
{
public void onRequest(RHTTPRequest request)
{
try
{
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
client.deliver(response);
}
catch (Exception x)
{
exception.set(x);
}
}
});
client.connect();
try
{
// Make a request to the gateway and check response
ContentExchange exchange = new ContentExchange(true);
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(address);
exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
String requestContent = "body";
exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
httpClient.send(exchange);
int status = exchange.waitForDone();
assertEquals(HttpExchange.STATUS_COMPLETED, status);
assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
assertNull(exception.get());
suspendLatch.await();
assertFalse(suspended.get());
}
finally
{
client.disconnect();
}
}
finally
{
httpClient.stop();
}
}
finally
{
server.stop();
}
}
private class SlowToSuspendExternalRequest implements ExternalRequest
{
private final ExternalRequest delegate;
private final CountDownLatch respondLatch;
private final CountDownLatch suspendLatch;
private final AtomicBoolean suspended;
private SlowToSuspendExternalRequest(ExternalRequest delegate, CountDownLatch respondLatch, CountDownLatch suspendLatch, AtomicBoolean suspended)
{
this.delegate = delegate;
this.respondLatch = respondLatch;
this.suspendLatch = suspendLatch;
this.suspended = suspended;
}
public boolean suspend()
{
try
{
respondLatch.await();
boolean result = delegate.suspend();
suspended.set(result);
suspendLatch.countDown();
return result;
}
catch (InterruptedException x)
{
throw new AssertionError(x);
}
}
public void respond(RHTTPResponse response) throws IOException
{
delegate.respond(response);
respondLatch.countDown();
}
public RHTTPRequest getRequest()
{
return delegate.getRequest();
}
}
}

View File

@ -0,0 +1,126 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletResponse;
import junit.framework.TestCase;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPListener;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class ExternalTimeoutTest extends TestCase
{
public void testExternalTimeout() throws Exception
{
GatewayServer server = new GatewayServer();
SelectChannelConnector connector = new SelectChannelConnector();
server.addConnector(connector);
final long externalTimeout = 5000L;
((StandardGateway)server.getGateway()).setExternalTimeout(externalTimeout);
server.start();
try
{
Address address = new Address("localhost", connector.getLocalPort());
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
try
{
String targetId = "1";
final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
final AtomicReference<Integer> requestId = new AtomicReference<Integer>();
final AtomicReference<Exception> exceptionRef = new AtomicReference<Exception>();
client.addListener(new RHTTPListener()
{
public void onRequest(RHTTPRequest request)
{
try
{
// Save the request id
requestId.set(request.getId());
// Wait until external timeout expires
Thread.sleep(externalTimeout + 1000L);
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
client.deliver(response);
}
catch (Exception x)
{
exceptionRef.set(x);
}
}
});
client.connect();
try
{
// Make a request to the gateway and check response
ContentExchange exchange = new ContentExchange(true);
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(address);
exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
String requestContent = "body";
exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
httpClient.send(exchange);
int status = exchange.waitForDone();
assertEquals(HttpExchange.STATUS_COMPLETED, status);
assertEquals(HttpServletResponse.SC_GATEWAY_TIMEOUT, exchange.getResponseStatus());
assertNull(exceptionRef.get());
assertNull(server.getGateway().removeExternalRequest(requestId.get()));
}
finally
{
client.disconnect();
}
}
finally
{
httpClient.stop();
}
}
finally
{
server.stop();
}
}
}

View File

@ -0,0 +1,109 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPListener;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.rhttp.gateway.TargetIdRetriever;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class GatewayEchoServer
{
private volatile GatewayServer server;
private volatile Address address;
private volatile String uri;
private volatile HttpClient httpClient;
private volatile RHTTPClient client;
public void start() throws Exception
{
server = new GatewayServer();
Connector connector = new SelectChannelConnector();
server.addConnector(connector);
server.setTargetIdRetriever(new EchoTargetIdRetriever());
server.start();
server.dumpStdErr();
address = new Address("localhost", connector.getLocalPort());
uri = server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH;
httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
client = new JettyClient(httpClient, new Address("localhost", connector.getLocalPort()), server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, "echo");
client.addListener(new EchoListener(client));
client.connect();
}
public void stop() throws Exception
{
client.disconnect();
httpClient.stop();
server.stop();
}
public Address getAddress()
{
return address;
}
public String getURI()
{
return uri;
}
public static class EchoTargetIdRetriever implements TargetIdRetriever
{
public String retrieveTargetId(HttpServletRequest httpRequest)
{
return "echo";
}
}
private static class EchoListener implements RHTTPListener
{
private final RHTTPClient client;
public EchoListener(RHTTPClient client)
{
this.client = client;
}
public void onRequest(RHTTPRequest request) throws Exception
{
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
client.deliver(response);
}
}
}

View File

@ -0,0 +1,77 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import javax.servlet.http.HttpServletResponse;
import junit.framework.TestCase;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.io.ByteArrayBuffer;
/**
* @version $Revision$ $Date$
*/
public class GatewayEchoTest extends TestCase
{
/**
* Tests that the basic functionality of the gateway works,
* by issuing a request and by replying with the same body.
*
* @throws Exception in case of test exceptions
*/
public void testEcho() throws Exception
{
GatewayEchoServer server = new GatewayEchoServer();
server.start();
try
{
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
try
{
// Make a request to the gateway and check response
ContentExchange exchange = new ContentExchange(true);
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(server.getAddress());
exchange.setURI(server.getURI() + "/");
String requestBody = "body";
exchange.setRequestContent(new ByteArrayBuffer(requestBody.getBytes("UTF-8")));
httpClient.send(exchange);
int status = exchange.waitForDone();
assertEquals(HttpExchange.STATUS_COMPLETED, status);
assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
String responseContent = exchange.getResponseContent();
assertEquals(responseContent, requestBody);
}
finally
{
httpClient.stop();
}
}
finally
{
server.stop();
}
}
}

View File

@ -0,0 +1,202 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import javax.servlet.http.HttpServletResponse;
import junit.framework.TestCase;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.io.ByteArrayBuffer;
/**
* @version $Revision$ $Date$
*/
public class GatewayLoadTest extends TestCase
{
private final ConcurrentMap<Long, AtomicLong> latencies = new ConcurrentHashMap<Long, AtomicLong>();
private final AtomicLong responses = new AtomicLong(0L);
private final AtomicLong failures = new AtomicLong(0L);
private final AtomicLong minLatency = new AtomicLong(Long.MAX_VALUE);
private final AtomicLong maxLatency = new AtomicLong(0L);
private final AtomicLong totLatency = new AtomicLong(0L);
public void testEcho() throws Exception
{
GatewayEchoServer server = new GatewayEchoServer();
server.start();
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
String uri = server.getURI() + "/";
char[] chars = new char[1024];
Arrays.fill(chars, 'x');
String requestBody = new String(chars);
int count = 1000;
CountDownLatch latch = new CountDownLatch(count);
for (int i = 0; i < count; ++i)
{
GatewayLoadTestExchange exchange = new GatewayLoadTestExchange(latch);
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(server.getAddress());
exchange.setURI(uri + i);
exchange.setRequestContent(new ByteArrayBuffer(requestBody.getBytes("UTF-8")));
exchange.setStartNanos(System.nanoTime());
httpClient.send(exchange);
Thread.sleep(5);
}
latch.await();
printLatencies(count);
assertEquals(count, responses.get() + failures.get());
}
private void updateLatencies(long start, long end)
{
long latency = end - start;
// Update the latencies using a non-blocking algorithm
long oldMinLatency = minLatency.get();
while (latency < oldMinLatency)
{
if (minLatency.compareAndSet(oldMinLatency, latency)) break;
oldMinLatency = minLatency.get();
}
long oldMaxLatency = maxLatency.get();
while (latency > oldMaxLatency)
{
if (maxLatency.compareAndSet(oldMaxLatency, latency)) break;
oldMaxLatency = maxLatency.get();
}
totLatency.addAndGet(latency);
latencies.putIfAbsent(latency, new AtomicLong(0L));
latencies.get(latency).incrementAndGet();
}
public void printLatencies(long expectedCount)
{
if (latencies.size() > 1)
{
long maxLatencyBucketFrequency = 0L;
long[] latencyBucketFrequencies = new long[20];
long latencyRange = maxLatency.get() - minLatency.get();
for (Iterator<Map.Entry<Long, AtomicLong>> entries = latencies.entrySet().iterator(); entries.hasNext();)
{
Map.Entry<Long, AtomicLong> entry = entries.next();
long latency = entry.getKey();
Long bucketIndex = (latency - minLatency.get()) * latencyBucketFrequencies.length / latencyRange;
int index = bucketIndex.intValue() == latencyBucketFrequencies.length ? latencyBucketFrequencies.length - 1 : bucketIndex.intValue();
long value = entry.getValue().get();
latencyBucketFrequencies[index] += value;
if (latencyBucketFrequencies[index] > maxLatencyBucketFrequency) maxLatencyBucketFrequency = latencyBucketFrequencies[index];
entries.remove();
}
System.out.println("Messages - Latency Distribution Curve (X axis: Frequency, Y axis: Latency):");
for (int i = 0; i < latencyBucketFrequencies.length; i++)
{
long latencyBucketFrequency = latencyBucketFrequencies[i];
int value = Math.round(latencyBucketFrequency * (float) latencyBucketFrequencies.length / maxLatencyBucketFrequency);
if (value == latencyBucketFrequencies.length) value = value - 1;
for (int j = 0; j < value; ++j) System.out.print(" ");
System.out.print("@");
for (int j = value + 1; j < latencyBucketFrequencies.length; ++j) System.out.print(" ");
System.out.print(" _ ");
System.out.print(TimeUnit.NANOSECONDS.toMillis((latencyRange * (i + 1) / latencyBucketFrequencies.length) + minLatency.get()));
System.out.print(" (" + latencyBucketFrequency + ")");
System.out.println(" ms");
}
}
long responseCount = responses.get();
System.out.print("Messages success/failed/expected = ");
System.out.print(responseCount);
System.out.print("/");
System.out.print(failures.get());
System.out.print("/");
System.out.print(expectedCount);
System.out.print(" - Latency min/ave/max = ");
System.out.print(TimeUnit.NANOSECONDS.toMillis(minLatency.get()) + "/");
System.out.print(responseCount == 0 ? "-/" : TimeUnit.NANOSECONDS.toMillis(totLatency.get() / responseCount) + "/");
System.out.println(TimeUnit.NANOSECONDS.toMillis(maxLatency.get()) + " ms");
}
private class GatewayLoadTestExchange extends ContentExchange
{
private final CountDownLatch latch;
private volatile long start;
private GatewayLoadTestExchange(CountDownLatch latch)
{
super(true);
this.latch = latch;
}
@Override
protected void onResponseComplete() throws IOException
{
if (getResponseStatus() == HttpServletResponse.SC_OK)
{
long end = System.nanoTime();
responses.incrementAndGet();
updateLatencies(start, end);
}
else
{
failures.incrementAndGet();
}
latch.countDown();
}
@Override
protected void onException(Throwable throwable)
{
failures.incrementAndGet();
latch.countDown();
}
@Override
protected void onExpire()
{
failures.incrementAndGet();
latch.countDown();
}
public void setStartNanos(long value)
{
start = value;
}
}
}

View File

@ -0,0 +1,130 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletResponse;
import junit.framework.TestCase;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.client.RHTTPListener;
import org.eclipse.jetty.rhttp.client.RHTTPRequest;
import org.eclipse.jetty.rhttp.client.RHTTPResponse;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class GatewayTimeoutTest extends TestCase
{
/**
* Tests a forwarded request that lasts longer than the gateway timeout.
* The gateway client will perform 2 long polls before the forwarded request's response is returned.
*
* @throws Exception in case of test exceptions
*/
public void testGatewayTimeout() throws Exception
{
GatewayServer server = new GatewayServer();
Connector connector = new SelectChannelConnector();
server.addConnector(connector);
final long gatewayTimeout = 5000L;
((StandardGateway)server.getGateway()).setGatewayTimeout(gatewayTimeout);
final long externalTimeout = gatewayTimeout * 2;
((StandardGateway)server.getGateway()).setExternalTimeout(externalTimeout);
server.start();
try
{
Address address = new Address("localhost", connector.getLocalPort());
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
try
{
String targetId = "1";
final RHTTPClient client = new JettyClient(httpClient, address, server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, targetId);
final AtomicReference<Exception> exceptionRef = new AtomicReference<Exception>();
client.addListener(new RHTTPListener()
{
public void onRequest(RHTTPRequest request)
{
try
{
// Wait until gateway timeout expires
Thread.sleep(gatewayTimeout + 1000L);
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
client.deliver(response);
}
catch (Exception x)
{
exceptionRef.set(x);
}
}
});
client.connect();
try
{
// Make a request to the gateway and check response
ContentExchange exchange = new ContentExchange(true);
exchange.setMethod(HttpMethods.POST);
exchange.setAddress(address);
exchange.setURI(server.getContext().getContextPath()+GatewayServer.DFT_EXT_PATH + "/" + URLEncoder.encode(targetId, "UTF-8"));
String requestContent = "body";
exchange.setRequestContent(new ByteArrayBuffer(requestContent.getBytes("UTF-8")));
httpClient.send(exchange);
int status = exchange.waitForDone();
assertEquals(HttpExchange.STATUS_COMPLETED, status);
assertEquals(HttpServletResponse.SC_OK, exchange.getResponseStatus());
assertNull(exceptionRef.get());
String responseContent = exchange.getResponseContent();
assertEquals(responseContent, requestContent);
}
finally
{
client.disconnect();
}
}
finally
{
httpClient.stop();
}
}
finally
{
server.stop();
}
}
}

View File

@ -0,0 +1,75 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import junit.framework.TestCase;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.rhttp.client.JettyClient;
import org.eclipse.jetty.rhttp.client.RHTTPClient;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.rhttp.gateway.StandardGateway;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class HandshakeClientTest extends TestCase
{
public void testConnectReturnsImmediately() throws Exception
{
GatewayServer server = new GatewayServer();
SelectChannelConnector connector = new SelectChannelConnector();
server.addConnector(connector);
long gwt=5000L;
((StandardGateway)server.getGateway()).setGatewayTimeout(gwt);
server.start();
try
{
HttpClient httpClient = new HttpClient();
httpClient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
httpClient.start();
try
{
RHTTPClient client = new JettyClient(httpClient, new Address("localhost", connector.getLocalPort()), server.getContext().getContextPath()+GatewayServer.DFT_CONNECT_PATH, "test1");
long start = System.currentTimeMillis();
client.connect();
try
{
long end = System.currentTimeMillis();
assertTrue(end - start < gwt / 2);
}
finally
{
client.disconnect();
}
}
finally
{
httpClient.stop();
}
}
finally
{
server.stop();
}
}
}

View File

@ -0,0 +1,107 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.gateway;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jetty.rhttp.gateway.HostTargetIdRetriever;
import junit.framework.TestCase;
/**
* @version $Revision$ $Date$
*/
public class HostTargetIdRetrieverTest extends TestCase
{
public void testHostTargetIdRetrieverNoSuffix()
{
String host = "test";
Class<HttpServletRequest> klass = HttpServletRequest.class;
HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class<?>[]{klass}, new Request(host));
HostTargetIdRetriever retriever = new HostTargetIdRetriever(null);
String result = retriever.retrieveTargetId(request);
assertEquals(host, result);
}
public void testHostTargetIdRetrieverWithSuffix()
{
String suffix = ".rhttp.example.com";
String host = "test";
Class<HttpServletRequest> klass = HttpServletRequest.class;
HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class<?>[]{klass}, new Request(host + suffix));
HostTargetIdRetriever retriever = new HostTargetIdRetriever(suffix);
String result = retriever.retrieveTargetId(request);
assertEquals(host, result);
}
public void testHostTargetIdRetrieverWithSuffixAndPort()
{
String suffix = ".rhttp.example.com";
String host = "test";
Class<HttpServletRequest> klass = HttpServletRequest.class;
HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class<?>[]{klass}, new Request(host + suffix + ":8080"));
HostTargetIdRetriever retriever = new HostTargetIdRetriever(suffix);
String result = retriever.retrieveTargetId(request);
assertEquals(host, result);
}
public void testHostTargetIdRetrieverNullHost()
{
Class<HttpServletRequest> klass = HttpServletRequest.class;
HttpServletRequest request = (HttpServletRequest)Proxy.newProxyInstance(klass.getClassLoader(), new Class<?>[]{klass}, new Request(null));
HostTargetIdRetriever retriever = new HostTargetIdRetriever(".rhttp.example.com");
String result = retriever.retrieveTargetId(request);
assertNull(result);
}
private static class Request implements InvocationHandler
{
private final String host;
private Request(String host)
{
this.host = host;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
if ("getHeader".equals(method.getName()))
{
if (args.length == 1 && "Host".equals(args[0]))
{
return host;
}
}
return null;
}
}
}

View File

@ -0,0 +1,13 @@
# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
#
log4j.rootLogger=ALL,CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
#log4j.appender.CONSOLE.threshold=INFO
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n
log4j.appender.CONSOLE.layout.ConversionPattern=%d [%5p][%c] %m%n
# Level tuning
log4j.logger.org.eclipse.jetty=INFO
log4j.logger.org.mortbay.jetty.rhttp=INFO

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.eclipse.jetty.rhttp</groupId>
<artifactId>jetty-rhttp-project</artifactId>
<version>9.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>reverse-http-loadtest</artifactId>
<packaging>jar</packaging>
<name>Jetty :: Reverse HTTP :: Load Test</name>
<profiles>
<profile>
<id>loader</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>org.eclipse.jetty.rhttp.loadtest.Loader</mainClass>
<classpathScope>runtime</classpathScope>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>server</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<mainClass>org.eclipse.jetty.rhttp.loadtest.Server</mainClass>
<classpathScope>runtime</classpathScope>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>reverse-http-gateway</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,429 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.loadtest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.eclipse.jetty.client.Address;
import org.eclipse.jetty.client.ContentExchange;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.mortbay.jetty.rhttp.client.RHTTPClient;
import org.mortbay.jetty.rhttp.client.JettyClient;
import org.mortbay.jetty.rhttp.client.RHTTPListener;
import org.mortbay.jetty.rhttp.client.RHTTPRequest;
import org.mortbay.jetty.rhttp.client.RHTTPResponse;
/**
* @version $Revision$ $Date$
*/
public class Loader
{
private final List<RHTTPClient> clients = new ArrayList<RHTTPClient>();
private final AtomicLong start = new AtomicLong();
private final AtomicLong end = new AtomicLong();
private final AtomicLong responses = new AtomicLong();
private final AtomicLong failures = new AtomicLong();
private final AtomicLong minLatency = new AtomicLong();
private final AtomicLong maxLatency = new AtomicLong();
private final AtomicLong totLatency = new AtomicLong();
private final ConcurrentMap<Long, AtomicLong> latencies = new ConcurrentHashMap<Long, AtomicLong>();
private final String nodeName;
public static void main(String[] args) throws Exception
{
String nodeName = "";
if (args.length > 0)
nodeName = args[0];
Loader loader = new Loader(nodeName);
loader.run();
}
public Loader(String nodeName)
{
this.nodeName = nodeName;
}
private void run() throws Exception
{
HttpClient httpClient = new HttpClient();
httpClient.setMaxConnectionsPerAddress(40000);
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setMaxThreads(500);
threadPool.setDaemon(true);
httpClient.setThreadPool(threadPool);
httpClient.setIdleTimeout(5000);
httpClient.start();
Random random = new Random();
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
System.err.print("server [localhost]: ");
String value = console.readLine().trim();
if (value.length() == 0)
value = "localhost";
String host = value;
System.err.print("port [8080]: ");
value = console.readLine().trim();
if (value.length() == 0)
value = "8080";
int port = Integer.parseInt(value);
System.err.print("context []: ");
value = console.readLine().trim();
if (value.length() == 0)
value = "";
String context = value;
System.err.print("external path [/]: ");
value = console.readLine().trim();
if (value.length() == 0)
value = "/";
String externalPath = value;
System.err.print("gateway path [/__gateway]: ");
value = console.readLine().trim();
if (value.length() == 0)
value = "/__gateway";
String gatewayPath = value;
int clients = 100;
int batchCount = 1000;
int batchSize = 5;
long batchPause = 5;
int requestSize = 50;
while (true)
{
System.err.println("-----");
System.err.print("clients [" + clients + "]: ");
value = console.readLine();
if (value == null)
break;
value = value.trim();
if (value.length() == 0)
value = "" + clients;
clients = Integer.parseInt(value);
System.err.println("Waiting for clients to be ready...");
Address gatewayAddress = new Address(host, port);
String gatewayURI = context + gatewayPath;
// Create or remove the necessary clients
int currentClients = this.clients.size();
if (currentClients < clients)
{
for (int i = 0; i < clients - currentClients; ++i)
{
final RHTTPClient client = new JettyClient(httpClient, gatewayAddress, gatewayURI, nodeName + (currentClients + i));
client.addListener(new EchoListener(client));
client.connect();
this.clients.add(client);
// Give some time to the server to accept connections and
// reply to handshakes and connects
if (i % 10 == 0)
{
Thread.sleep(100);
}
}
}
else if (currentClients > clients)
{
for (int i = 0; i < currentClients - clients; ++i)
{
RHTTPClient client = this.clients.remove(currentClients - i - 1);
client.disconnect();
}
}
System.err.println("Clients ready");
currentClients = this.clients.size();
if (currentClients > 0)
{
System.err.print("batch count [" + batchCount + "]: ");
value = console.readLine().trim();
if (value.length() == 0)
value = "" + batchCount;
batchCount = Integer.parseInt(value);
System.err.print("batch size [" + batchSize + "]: ");
value = console.readLine().trim();
if (value.length() == 0)
value = "" + batchSize;
batchSize = Integer.parseInt(value);
System.err.print("batch pause [" + batchPause + "]: ");
value = console.readLine().trim();
if (value.length() == 0)
value = "" + batchPause;
batchPause = Long.parseLong(value);
System.err.print("request size [" + requestSize + "]: ");
value = console.readLine().trim();
if (value.length() == 0)
value = "" + requestSize;
requestSize = Integer.parseInt(value);
String requestBody = "";
for (int i = 0; i < requestSize; i++)
requestBody += "x";
String externalURL = "http://" + host + ":" + port + context + externalPath;
if (!externalURL.endsWith("/"))
externalURL += "/";
reset();
long start = System.nanoTime();
long expected = 0;
for (int i = 0; i < batchCount; ++i)
{
for (int j = 0; j < batchSize; ++j)
{
int clientIndex = random.nextInt(this.clients.size());
RHTTPClient client = this.clients.get(clientIndex);
String targetId = client.getTargetId();
String url = externalURL + targetId;
ExternalExchange exchange = new ExternalExchange();
exchange.setMethod("GET");
exchange.setURL(url);
exchange.setRequestContent(new ByteArrayBuffer(requestBody, "UTF-8"));
exchange.send(httpClient);
++expected;
}
if (batchPause > 0)
Thread.sleep(batchPause);
}
long end = System.nanoTime();
long elapsedNanos = end - start;
if (elapsedNanos > 0)
{
System.err.print("Messages - Elapsed | Rate = ");
System.err.print(TimeUnit.NANOSECONDS.toMillis(elapsedNanos));
System.err.print(" ms | ");
System.err.print(expected * 1000 * 1000 * 1000 / elapsedNanos);
System.err.println(" requests/s ");
}
waitForResponses(expected);
printReport(expected);
}
}
}
private void reset()
{
start.set(0L);
end.set(0L);
responses.set(0L);
failures.set(0L);
minLatency.set(Long.MAX_VALUE);
maxLatency.set(0L);
totLatency.set(0L);
}
private void updateLatencies(long start, long end)
{
long latency = end - start;
// Update the latencies using a non-blocking algorithm
long oldMinLatency = minLatency.get();
while (latency < oldMinLatency)
{
if (minLatency.compareAndSet(oldMinLatency, latency)) break;
oldMinLatency = minLatency.get();
}
long oldMaxLatency = maxLatency.get();
while (latency > oldMaxLatency)
{
if (maxLatency.compareAndSet(oldMaxLatency, latency)) break;
oldMaxLatency = maxLatency.get();
}
totLatency.addAndGet(latency);
latencies.putIfAbsent(latency, new AtomicLong(0L));
latencies.get(latency).incrementAndGet();
}
private boolean waitForResponses(long expected) throws InterruptedException
{
long arrived = responses.get() + failures.get();
long lastArrived = 0;
int maxRetries = 20;
int retries = maxRetries;
while (arrived < expected)
{
System.err.println("Waiting for responses to arrive " + arrived + "/" + expected);
Thread.sleep(500);
if (lastArrived == arrived)
{
--retries;
if (retries == 0) break;
}
else
{
lastArrived = arrived;
retries = maxRetries;
}
arrived = responses.get() + failures.get();
}
if (arrived < expected)
{
System.err.println("Interrupting wait for responses " + arrived + "/" + expected);
return false;
}
else
{
System.err.println("All responses arrived " + arrived + "/" + expected);
return true;
}
}
public void printReport(long expectedCount)
{
long responseCount = responses.get() + failures.get();
System.err.print("Messages - Success/Failures/Expected = ");
System.err.print(responses.get());
System.err.print("/");
System.err.print(failures.get());
System.err.print("/");
System.err.println(expectedCount);
long elapsedNanos = end.get() - start.get();
if (elapsedNanos > 0)
{
System.err.print("Messages - Elapsed | Rate = ");
System.err.print(TimeUnit.NANOSECONDS.toMillis(elapsedNanos));
System.err.print(" ms | ");
System.err.print(responseCount * 1000 * 1000 * 1000 / elapsedNanos);
System.err.println(" responses/s ");
}
if (latencies.size() > 1)
{
long maxLatencyBucketFrequency = 0L;
long[] latencyBucketFrequencies = new long[20];
long latencyRange = maxLatency.get() - minLatency.get();
for (Iterator<Map.Entry<Long, AtomicLong>> entries = latencies.entrySet().iterator(); entries.hasNext();)
{
Map.Entry<Long, AtomicLong> entry = entries.next();
long latency = entry.getKey();
Long bucketIndex = (latency - minLatency.get()) * latencyBucketFrequencies.length / latencyRange;
int index = bucketIndex.intValue() == latencyBucketFrequencies.length ? latencyBucketFrequencies.length - 1 : bucketIndex.intValue();
long value = entry.getValue().get();
latencyBucketFrequencies[index] += value;
if (latencyBucketFrequencies[index] > maxLatencyBucketFrequency) maxLatencyBucketFrequency = latencyBucketFrequencies[index];
entries.remove();
}
System.err.println("Messages - Latency Distribution Curve (X axis: Frequency, Y axis: Latency):");
for (int i = 0; i < latencyBucketFrequencies.length; i++)
{
long latencyBucketFrequency = latencyBucketFrequencies[i];
int value = Math.round(latencyBucketFrequency * (float) latencyBucketFrequencies.length / maxLatencyBucketFrequency);
if (value == latencyBucketFrequencies.length) value = value - 1;
for (int j = 0; j < value; ++j) System.err.print(" ");
System.err.print("@");
for (int j = value + 1; j < latencyBucketFrequencies.length; ++j) System.err.print(" ");
System.err.print(" _ ");
System.err.print(TimeUnit.NANOSECONDS.toMillis((latencyRange * (i + 1) / latencyBucketFrequencies.length) + minLatency.get()));
System.err.println(" ms (" + latencyBucketFrequency + ")");
}
}
System.err.print("Messages - Latency Min/Ave/Max = ");
System.err.print(TimeUnit.NANOSECONDS.toMillis(minLatency.get()) + "/");
System.err.print(responseCount == 0 ? "-/" : TimeUnit.NANOSECONDS.toMillis(totLatency.get() / responseCount) + "/");
System.err.println(TimeUnit.NANOSECONDS.toMillis(maxLatency.get()) + " ms");
}
private class ExternalExchange extends ContentExchange
{
private volatile long sendTime;
private ExternalExchange()
{
super(true);
}
private void send(HttpClient httpClient) throws IOException
{
this.sendTime = System.nanoTime();
httpClient.send(this);
}
@Override
protected void onResponseComplete() throws IOException
{
if (getResponseStatus() == 200)
responses.incrementAndGet();
else
failures.incrementAndGet();
long arrivalTime = System.nanoTime();
if (start.get() == 0L)
start.set(arrivalTime);
end.set(arrivalTime);
updateLatencies(sendTime, arrivalTime);
}
@Override
protected void onException(Throwable x)
{
failures.incrementAndGet();
}
}
private static class EchoListener implements RHTTPListener
{
private final RHTTPClient client;
public EchoListener(RHTTPClient client)
{
this.client = client;
}
public void onRequest(RHTTPRequest request) throws Exception
{
RHTTPResponse response = new RHTTPResponse(request.getId(), 200, "OK", new HashMap<String, String>(), request.getBody());
client.deliver(response);
}
}
}

View File

@ -0,0 +1,69 @@
//
// ========================================================================
// Copyright (c) 1995-2012 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.rhttp.loadtest;
import org.eclipse.jetty.rhttp.gateway.GatewayServer;
import org.eclipse.jetty.rhttp.gateway.StandardTargetIdRetriever;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
/**
* @version $Revision$ $Date$
*/
public class Server
{
public static void main(String[] args) throws Exception
{
int port = 8080;
if (args.length > 0)
port = Integer.parseInt(args[0]);
GatewayServer server = new GatewayServer();
Connector connector = new SelectChannelConnector();
connector.setLowResourceMaxIdleTime(connector.getMaxIdleTime());
connector.setPort(port);
server.addConnector(connector);
server.setTargetIdRetriever(new StandardTargetIdRetriever());
server.start();
server.getServer().dumpStdErr();
Runtime.getRuntime().addShutdownHook(new Shutdown(server));
}
private static class Shutdown extends Thread
{
private final GatewayServer server;
public Shutdown(GatewayServer server)
{
this.server = server;
}
@Override
public void run()
{
try
{
server.stop();
}
catch (Exception ignored)
{
}
}
}
}

View File

@ -0,0 +1,13 @@
# LOG4J levels: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
#
log4j.rootLogger=ALL,CONSOLE
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
#log4j.appender.CONSOLE.threshold=INFO
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
#log4j.appender.CONSOLE.layout.ConversionPattern=%d %t [%5p][%c{1}] %m%n
log4j.appender.CONSOLE.layout.ConversionPattern=%d [%5p][%c] %m%n
# Level tuning
log4j.logger.org.eclipse.jetty=INFO
log4j.logger.org.mortbay.jetty.rhttp=INFO

108
jetty-rhttp/pom.xml Normal file
View File

@ -0,0 +1,108 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-project</artifactId>
<version>9.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse.jetty.rhttp</groupId>
<artifactId>jetty-rhttp-project</artifactId>
<packaging>pom</packaging>
<name>Jetty :: Reverse HTTP</name>
<properties>
</properties>
<modules>
<module>reverse-http-client</module>
<module>reverse-http-connector</module>
<module>reverse-http-gateway</module>
<module>reverse-http-loadtest</module>
</modules>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.maven.plugin</groupId>
<artifactId>emma-maven-plugin</artifactId>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>instrument</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<classesDirectory>${project.build.directory}/generated-classes/emma/classes</classesDirectory>
<systemProperties>
<property>
<name>emma.coverage.out.file</name>
<value>${project.build.directory}/coverage.ec</value>
</property>
</systemProperties>
</configuration>
</plugin>
<plugin>
<groupId>org.sonatype.maven.plugin</groupId>
<artifactId>emma4it-maven-plugin</artifactId>
<executions>
<execution>
<id>report</id>
<phase>post-integration-test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<sourceSets>
<sourceSet>
<directory>${project.build.sourceDirectory}</directory>
</sourceSet>
</sourceSets>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-io</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<groupId>org.eclipse.jetty.rhttp</groupId>
</project>