/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.server.unification.pipeline.http.proxy;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.helios.apmrouter.server.services.session.ChannelType;
import org.helios.apmrouter.server.services.session.SharedChannelGroup;
import org.helios.apmrouter.server.unification.pipeline.http.AbstractHttpRequestHandler;
import org.helios.apmrouter.server.unification.pipeline.http.HttpRequestHandlerStarted;
import org.helios.apmrouter.server.unification.pipeline.http.HttpRequestHandlerStopped;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.util.CharsetUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.support.MetricType;
/**
* <p>Title: HttpRequestProxy</p>
* <p>Description: An http handler implementation that asynchronously dispatches received requests to the configured remote server.</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.server.unification.pipeline.http.HttpRequestProxy</code></p>
*/
public class HttpRequestProxy extends AbstractHttpRequestHandler {
/** The host to proxy for */
protected String targetHost = null;
/** The port to proxy for */
protected int targetPort = -1;
/** The remote key */
protected String remoteKey = null;
/** The channel factory for proxies */
protected ProxyChannelFactory channelFactory = null;
/** A cache of proxied connections */
protected static final Map<String, Channel> proxyConnections = new ConcurrentHashMap<String, Channel>();
/** A map of URI remappings */
protected final Map<String, String> remaps = new ConcurrentHashMap<String, String>();
/** A counter to track the number of in-flight requests */
protected final AtomicInteger inFlightRequests = new AtomicInteger();
/** A counter for outgoing responses */
protected final AtomicLong outgoingResponses = new AtomicLong();
/** Traffic lock */
protected final Object trafficLock = new Object();
/**
* Creates a new HttpRequestProxy
*/
public HttpRequestProxy() {
super();
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponentBean#doStart()
*/
@Override
protected void doStart() throws Exception {
remoteKey = targetHost + ":" + targetPort;
applicationContext.publishEvent(new HttpRequestHandlerStarted(this, beanName));
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponent#resetMetrics()
*/
@Override
@ManagedOperation
public void resetMetrics() {
outgoingResponses.set(0);
inFlightRequests.set(0);
super.resetMetrics();
}
/**
* Adds the passed remaps to the proxy's remappers
* @param remaps A map of remapping directives where the string in the key will be replaced by the string in the value.
*/
public void setRemaps(Map<String, String> remaps) {
if(remaps!=null) {
this.remaps.putAll(remaps);
}
}
/**
* Returns an unmodifiable map of the proxu URI remap directives
* @return an unmodifiable map of the proxu URI remap directives
*/
@ManagedAttribute(description="A map of the proxu URI remap directives")
public Map<String, String> getRemaps() {
return Collections.unmodifiableMap(remaps);
}
/**
* Adds a remap
* @param from The value in the URI to replace
* @param to The value to replace with
*/
@ManagedOperation(description="Adds or replaces a proxy URI remap")
@ManagedOperationParameters({
@ManagedOperationParameter(name="from", description="The value in the URI to replace"),
@ManagedOperationParameter(name="to", description="The value to replace with")
})
public void addRemap(String from, String to) {
if(from==null) throw new IllegalArgumentException("The passed from value was null", new Throwable());
if(to==null) to="";
remaps.put(from, to);
}
/**
* Removes a remap directive
* @param from The key of the remap to remove
*/
@ManagedOperation(description="Removes a proxy URI remap")
@ManagedOperationParameters({
@ManagedOperationParameter(name="from", description="The key of the proxy URI remap")
})
public void removeRemap(String from) {
if(from!=null) {
remaps.remove(from);
}
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.ServerComponentBean#doStop()
*/
@Override
protected void doStop() {
applicationContext.publishEvent(new HttpRequestHandlerStopped(this, beanName));
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.server.unification.pipeline.http.HttpRequestHandler#handle(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.MessageEvent, org.jboss.netty.handler.codec.http.HttpRequest, java.lang.String)
*/
@Override
public void handle(final ChannelHandlerContext ctx, MessageEvent e, HttpRequest request, String path) throws Exception {
incr("IncomingRequests"); inFlightRequests.incrementAndGet();
final HttpRequest newRequest = new DefaultHttpRequest(request.getProtocolVersion(), request.getMethod(), remapUri(request));
newRequest.setContent(request.getContent());
for(String hdr: request.getHeaderNames()) {
if("Host".equalsIgnoreCase(hdr)) continue;
newRequest.setHeader(hdr, request.getHeader(hdr));
}
newRequest.setHeader("Host", targetHost + ":" + targetPort);
newRequest.setChunked(request.isChunked());
debug("Sending HttpRequest [\n", newRequest,"\n] to [", remoteKey, "]");
Channel proxyChannel = proxyConnections.get(remoteKey);
if(proxyChannel==null) {
synchronized(proxyConnections) {
proxyChannel = proxyConnections.get(remoteKey);
if(proxyChannel==null) {
Runnable onConnectRunnable = new Runnable() {
@Override
public void run() {
processProxyRequest(ctx, ctx.getChannel(), proxyConnections.get(remoteKey), newRequest);
}
};
getProxyConnection(ctx, onConnectRunnable);
}
}
}
if(proxyChannel!=null) {
processProxyRequest(ctx, ctx.getChannel(), proxyChannel, newRequest);
}
}
/**
* Determines if the passed request has an applicable remap and returns the remaped URI if one is found. Otherwise returns the un-modified uri.
* @param request The Http request to remap
* @return the remaped uri if a remap was found, otherwise the un-modified uri.
*/
protected String remapUri(HttpRequest request) {
String uri = request.getUri();
for(Map.Entry<String, String> remap: remaps.entrySet()) {
if(uri.startsWith(remap.getKey())) {
return uri.replace(remap.getKey(), remap.getValue());
}
}
return uri;
}
/**
* Asynchronously acquires a connection to the proxied server
* @param originalCtx The channel handler context of the original request
* @param onConnectRunnable A task to run once the connection has been acquired
*/
protected void getProxyConnection(final ChannelHandlerContext originalCtx, final Runnable onConnectRunnable) {
channelFactory.newChannelAsynch(targetHost, targetPort).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f1) throws Exception {
if(!f1.isSuccess()) {
error("Failed to connect proxy to remote at [", targetHost, ":", targetPort, "]", f1.getCause());
sendError(originalCtx, HttpResponseStatus.SERVICE_UNAVAILABLE);
} else {
debug("Connected proxy to remote at [", remoteKey, "]");
final Channel proxyChannel = f1.getChannel();
proxyChannel.getPipeline().addLast("responseHandler", new ProxyResponseHandler(targetHost, targetPort, inFlightRequests, outgoingResponses, trafficLock));
Channel priorChannel = proxyConnections.put(remoteKey, proxyChannel);
if(priorChannel!=null) priorChannel.close();
SharedChannelGroup.getInstance().add(proxyChannel, ChannelType.LOCAL_CLIENT, "ProxyTo[" + remoteKey + "]", "", "");
proxyChannel.getCloseFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
Channel closedChannel = f.getChannel();
Channel cachedChannel = proxyConnections.get(remoteKey);
if(cachedChannel!=null && closedChannel.getId().equals(cachedChannel.getId())) {
proxyConnections.remove(remoteKey);
}
}
});
if(onConnectRunnable!=null) {
onConnectRunnable.run();
}
}
}
});
}
/**
* Sends the original request to the proxied server. The response will be handled by the <code>responseHandler</code> installed into the pipeline.
* @param originalCtx The original request channel handler context
* @param originalChannel The original request channel handler context
* @param proxyChannel The connection to the proxied server
* @param request The modified Http request
*/
protected void processProxyRequest(final ChannelHandlerContext originalCtx, final Channel originalChannel, final Channel proxyChannel, final HttpRequest request) {
proxyChannel.getPipeline().getContext("responseHandler").setAttachment(originalCtx);
ProxyResponseHandler.httpRequestChannelLocal.set(proxyChannel, request);
ProxyResponseHandler.ctxChannelLocal.set(proxyChannel, originalCtx);
synchronized(trafficLock) {
proxyChannel.write(request).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if(!future.isSuccess()) {
incr("ProxyError");
}
}
});
if (!proxyChannel.isWritable()) {
info("PROXY CHANNEL SATURATED !!");
originalChannel.setReadable(false);
}
}
}
/**
* Returns an HTTP error back to the caller
* @param ctx The channel handler context
* @param status The HTTP Status to send
*/
private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
incr("ProxyError");
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status);
response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8");
response.setContent(ChannelBuffers.copiedBuffer(
"Failure: " + status.toString() + "\r\n",
CharsetUtil.UTF_8));
// Close the connection as soon as the error message is sent, or maybe not.....
ctx.getChannel().write(response); //.addListener(ChannelFutureListener.CLOSE);
}
/**
* Returns the cummulative number of outgoing responses
* @return the cummulative number of outgoing responses
*/
@ManagedMetric(category="HttpProxy", displayName="OutgoingResponseCount", metricType=MetricType.COUNTER, description="The cummulative number of outgoing responses")
public long getOutgoingResponseCount() {
return outgoingResponses.get();
}
/**
* Returns the cummulative number of incoming requests
* @return the cummulative number of incoming requests
*/
@ManagedMetric(category="HttpProxy", displayName="IncomingRequestCount", metricType=MetricType.COUNTER, description="The cummulative number of incoming requests")
public long getIncomingRequestCount() {
return getMetricValue("IncomingRequests");
}
/**
* Returns the number of in-flight requests
* @return the number of in-flight requests
*/
@ManagedMetric(category="HttpProxy", displayName="InFlightRequests", metricType=MetricType.GAUGE, description="The number of in-flight requests")
public int getInFlightRequests() {
return inFlightRequests.get();
}
/**
* Returns the cummulative number of proxy errors
* @return the cummulative number of proxy errors
*/
@ManagedMetric(category="HttpProxy", displayName="ProxyErrorCount", metricType=MetricType.COUNTER, description="The cummulative number of proxy errors")
public long getProxyErrorCount() {
return getMetricValue("ProxyError");
}
/**
* Returns the target host
* @return the targetHost
*/
@ManagedAttribute(description="The target host for this proxy")
public String getTargetHost() {
return targetHost;
}
/**
* Returns the number of proxy connections
* @return the number of proxy connections
*/
@ManagedMetric(category="HttpProxy", displayName="ProxyConnectionCount", metricType=MetricType.GAUGE, description="The current number of proxy connections")
public int getProxyConnectionCount() {
return proxyConnections.size();
}
/**
* Sets the target host
* @param targetHost the targetHost to set
*/
public void setTargetHost(String targetHost) {
this.targetHost = targetHost;
}
/**
* Returns the target port
* @return the targetPort
*/
@ManagedAttribute(description="The target port for this proxy")
public int getTargetPort() {
return targetPort;
}
/**
* Sets the target port
* @param targetPort the targetPort to set
*/
public void setTargetPort(int targetPort) {
this.targetPort = targetPort;
}
/**
* Sets the proxy client channel factory
* @param channelFactory the channelFactory to set
*/
@Autowired(required=true)
public void setChannelFactory(ProxyChannelFactory channelFactory) {
this.channelFactory = channelFactory;
}
}
/*
OLD IMPL.
=========
//processProxyRequest(final ChannelHandlerContext originalCtx, final Channel originalChannel, final Channel proxyChannel, final HttpRequest request)
// channelFactory.newChannelAsynch(targetHost, targetPort).addListener(new ChannelFutureListener() {
// @Override
// public void operationComplete(ChannelFuture f1) throws Exception {
// if(!f1.isSuccess()) {
// error("Failed to connect proxy to remote at [", targetHost, ":", targetPort, "]", f1.getCause());
// sendError(ctx, HttpResponseStatus.SERVICE_UNAVAILABLE);
// } else {
// debug("Connected proxy to remote at [", remoteKey, "]");
// final Channel clientChannel = f1.getChannel();
// SharedChannelGroup.getInstance().add(clientChannel, ChannelType.LOCAL_CLIENT, "ProxyTo[" + remoteKey + "]", "", "");
// clientChannel.getPipeline().addLast("proxyResponder", new ChannelUpstreamHandler() {
// @Override
// public void handleUpstream(ChannelHandlerContext ct, ChannelEvent e) throws Exception {
// if(e instanceof MessageEvent) {
// MessageEvent me = (MessageEvent)e;
// Object message = me.getMessage();
// if(message instanceof HttpResponse) {
// debug("Received response from remote [", message, "]");
// HttpResponse resp = (HttpResponse)message;
// if(resp.getStatus().equals(HttpResponseStatus.FOUND)) {
// String reUri = resp.getHeader("Location");
// reUri = reUri.substring(reUri.indexOf("" + targetPort)+(""+targetPort).length());
// newRequest.setUri(reUri);
// clientChannel.write(newRequest);
// } else {
// Channel ch = ctx.getChannel();
// ChannelFuture cf = Channels.future(ch);
// ctx.sendDownstream(new DownstreamMessageEvent(ch, cf, resp, ch.getRemoteAddress()));
// cf.addListener(new ChannelFutureListener() {
// @Override
// public void operationComplete(ChannelFuture f3) throws Exception {
// if(f3.isSuccess()) {
// debug("Completed response write back to caller");
// } else {
// error("Failed to write response back to caller", f3.getCause());
// f3.getCause().printStackTrace(System.err);
// }
// }
// });
// }
// return;
// }
// }
// ct.sendUpstream(e);
// }
// });
// clientChannel.write(newRequest).addListener(new ChannelFutureListener() {
// @Override
// public void operationComplete(ChannelFuture f2) throws Exception {
// if(f2.isSuccess()) {
// debug("Completed proxy write to remote at [", remoteKey, "]");
// } else {
// error("Failed to write request to remote at [", remoteKey, "]", f2.getCause());
// f2.getCause().printStackTrace(System.err);
// }
// }
// });
// }
// }
// });
*/