
Продолжаем знакомиться с netty. Представим себе такую задачу. Есть компонент системы, которые гоняется в облаке Amazon. Он может принимать HTTPS соединения, и мы хотим этим воспользоваться для шифрования передаваемого трафика. Но другой компонент системы умеет посылать только HTTP запросы и по независящим от нас причинам (к примеру, нет исходников) мы не можем научить его работать с HTTPS. Что тут можно сделать?
Я решил написать на netty своего рода прокси (или mapper), который возьмет на себя шифрование HTTP запросов и расшифровывание HTTPS ответов. Его можно запустить в своей собственной сети и тем самым добиться того, что в публичной сети трафик будет шифрованный.
Необходимая зависимость в POM:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
<version>3.6.6.Final</version>
</dependency> |
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
<version>3.6.6.Final</version>
</dependency>
Код класса, в котором запускается main():
package ru.outofrange.sslmapper;
import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
public class HttpsMapper {
public static void main(String[] args) {
int localPort = 8100; // default value, our application will attempt to run on this port
String remoteHost = "";
int remotePort = 443; // default port for HTTPS
HttpHost destHost = null;
if (args.length >= 3) {
localPort = Integer.parseInt(args[0]); // our JAR will listen on this port
remoteHost = args[1]; // our JAR will send requests there
remotePort = Integer.parseInt(args[2]);
} else {
System.out.println("Expecting three command line arguments: 1.listen port 2. Destination host 3. Destination port");
// exit early
System.exit(1);
}
System.out.println("Listening on port " + localPort);
System.out.println("Getting data from [" + remoteHost + "]:[" + remotePort +"]");
destHost = new HttpHost(remoteHost, remotePort);
Executor threadPool = Executors.newCachedThreadPool();
ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(threadPool, threadPool));
// creating a pipelineFactory
bootstrap.setPipelineFactory(new ProxyServerPipelineFactory(threadPool, destHost));
bootstrap.bind(new InetSocketAddress(localPort));
}
static HttpHost getUpstreamProxy() {
// I assume it's needed for a proxy chain, I don't need to implement it
return null;
}
} |
package ru.outofrange.sslmapper;
import java.net.InetSocketAddress;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
public class HttpsMapper {
public static void main(String[] args) {
int localPort = 8100; // default value, our application will attempt to run on this port
String remoteHost = "";
int remotePort = 443; // default port for HTTPS
HttpHost destHost = null;
if (args.length >= 3) {
localPort = Integer.parseInt(args[0]); // our JAR will listen on this port
remoteHost = args[1]; // our JAR will send requests there
remotePort = Integer.parseInt(args[2]);
} else {
System.out.println("Expecting three command line arguments: 1.listen port 2. Destination host 3. Destination port");
// exit early
System.exit(1);
}
System.out.println("Listening on port " + localPort);
System.out.println("Getting data from [" + remoteHost + "]:[" + remotePort +"]");
destHost = new HttpHost(remoteHost, remotePort);
Executor threadPool = Executors.newCachedThreadPool();
ServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(threadPool, threadPool));
// creating a pipelineFactory
bootstrap.setPipelineFactory(new ProxyServerPipelineFactory(threadPool, destHost));
bootstrap.bind(new InetSocketAddress(localPort));
}
static HttpHost getUpstreamProxy() {
// I assume it's needed for a proxy chain, I don't need to implement it
return null;
}
}
Класс, который обслуживает http запросы от клиента:
package ru.outofrange.sslmapper;
import java.util.concurrent.Executor;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
// this class handles requests from the client
public class ProxyServerPipelineFactory implements ChannelPipelineFactory {
private Executor threadPool = null;
private HttpHost destHost = null;
public ProxyServerPipelineFactory(Executor threadPool, HttpHost destHost) {
this.threadPool = threadPool;
this.destHost = destHost;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline p = Channels.pipeline();
p.addLast("decoder", new HttpRequestDecoder());
p.addLast("aggregator", new HttpChunkAggregator(1048576));
p.addLast("encoder", new HttpResponseEncoder());
// that's our custom handler
p.addLast("handler", new ProxyServerHandler(threadPool, destHost));
return p;
}
} |
package ru.outofrange.sslmapper;
import java.util.concurrent.Executor;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
// this class handles requests from the client
public class ProxyServerPipelineFactory implements ChannelPipelineFactory {
private Executor threadPool = null;
private HttpHost destHost = null;
public ProxyServerPipelineFactory(Executor threadPool, HttpHost destHost) {
this.threadPool = threadPool;
this.destHost = destHost;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline p = Channels.pipeline();
p.addLast("decoder", new HttpRequestDecoder());
p.addLast("aggregator", new HttpChunkAggregator(1048576));
p.addLast("encoder", new HttpResponseEncoder());
// that's our custom handler
p.addLast("handler", new ProxyServerHandler(threadPool, destHost));
return p;
}
}
Код хэндлера и необходимые пояснения к нему. При получении http запроса от клиента вызывается метод messageReceived(). В нем создается новый канал (pipeline), в который наряду с типовыми хэндлерами добавлен пользовательский RemoteHostHandler. Получение ответа от хэндлера происходит в вызове writeResponseAndClose(remoteHostHandler.getResponse()); . Собственно, код:
package ru.outofrange.sslmapper;
import java.net.*;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.*;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpResponseDecoder;
import org.jboss.netty.handler.codec.http.HttpRequestEncoder;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import javax.net.ssl.SSLEngine;
import org.jboss.netty.handler.ssl.SslHandler;
/*
Handler of the incoming connections
Receives the request, transforms to HTTP 1.1 if needed, establishes an outgoing connection to OS
and requests data from the OS. OS's response is unzipped if necessary and is forwarded to the client.
There will be a redirect if OS sends response 301 or 302.
*/
@ChannelPipelineCoverage("one")
public class ProxyServerHandler extends SimpleChannelUpstreamHandler {
private volatile Channel proxyServerChannel = null;
private Executor threadPool = null;
private String remoteHost = null;
private int remotePort = 80; // default value
private RemoteHostHandler remoteHostHandler = null;
private static AtomicLong requestCount = new AtomicLong(0);
public ProxyServerHandler(Executor threadPool) {
this.threadPool = threadPool;
}
public ProxyServerHandler(Executor threadPool, HttpHost destHost) {
this.threadPool = threadPool;
this.remoteHost = destHost.getHost();
this.remotePort = destHost.getPort();
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
HttpRequest request = (HttpRequest) e.getMessage();
long currRequestValue = requestCount.incrementAndGet();
if (currRequestValue % 100 == 0) {
// reporting every 100th request
System.out.println("Num Of HTTP Requests: " + currRequestValue);
}
// keep alive not supported
request.removeHeader("Proxy-Connection");
request.setHeader("Connection", "close");
if (request.getProtocolVersion() == HttpVersion.HTTP_1_0) {
request = convertRequestTo1_1(request);
}
// here we can process the request, e.g. detect specific URI and respond using
// writeResponseAndClose(ErrorResponse.create("unsupported uri")); and stop the processing
ClientBootstrap bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(threadPool, threadPool));
ChannelPipeline p = bootstrap.getPipeline();
// Add SSL handler first to encrypt and decrypt everything.
// In this example, we use a bogus certificate in the server side
// and accept any invalid certificates in the client side.
// You will need something more complicated to identify both
// and server in the real world
SSLEngine engine = HttpsMapperSslContextFactory.getClientContext().createSSLEngine();
engine.setUseClientMode(true);
// this one provides coding / encoding
p.addLast("ssl", new SslHandler(engine));
p.addLast("decoder", new HttpResponseDecoder());
p.addLast("aggregator", new HttpChunkAggregator(1048576));
p.addLast("encoder", new HttpRequestEncoder());
remoteHostHandler = new RemoteHostHandler(request); // depends on the client's request
p.addLast("handler", remoteHostHandler);
proxyServerChannel = e.getChannel();
HttpHost proxy = HttpsMapper.getUpstreamProxy(); // currently returns null, always
InetSocketAddress dest = proxy == null ?
new InetSocketAddress(remoteHost, remotePort) :
new InetSocketAddress(proxy.getHost(), proxy.getPort());
ChannelFuture connectFuture = bootstrap.connect(dest);
connectFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
future.getChannel().getCloseFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
writeResponseAndClose(remoteHostHandler.getResponse()); // here we get the response from OS and forward it to the client
}
});
} else {
future.getChannel().close();
writeResponseAndClose(createErrorResponse("Could not connect to " + remoteHost + ":" + remotePort));
}
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("Unexpected exception from proxy server handler: " + e.getCause());
e.getChannel().close();
}
// sends the generated response into incoming connection (channel between HttpsMapper and the client)
private void writeResponseAndClose(HttpResponse response) {
if (response != null) {
response.setHeader("Connection", "close");
proxyServerChannel.write(response).addListener(ChannelFutureListener.CLOSE);
} else {
proxyServerChannel.close();
}
}
// converts HTTP 1.0 requests into HTTP 1.1
private HttpRequest convertRequestTo1_1(HttpRequest request) throws URISyntaxException {
DefaultHttpRequest newReq = new DefaultHttpRequest(HttpVersion.HTTP_1_1, request.getMethod(), request.getUri());
if (!request.getHeaderNames().isEmpty()) {
for (String name : request.getHeaderNames()) {
newReq.setHeader(name, request.getHeaders(name));
}
}
if (!newReq.containsHeader(HttpHeaders.Names.HOST)) {
URI url = new URI(newReq.getUri());
String host = url.getHost();
if (url.getPort() != -1) {
host += ":" + url.getPort();
}
newReq.setHeader(HttpHeaders.Names.HOST, host);
}
newReq.setContent(request.getContent());
return newReq;
}
// creates an http response with a specified text
public static HttpResponse createErrorResponse(String errorText) {
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=utf-8");
ChannelBuffer buf = ChannelBuffers.copiedBuffer("<html><body><h3>" + errorText + "</h3></body></html>", "utf-8");
response.setContent(buf);
response.setHeader("Content-Length", String.valueOf(buf.readableBytes()));
return response;
}
} |
package ru.outofrange.sslmapper;
import java.net.*;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.*;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpResponseDecoder;
import org.jboss.netty.handler.codec.http.HttpRequestEncoder;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import javax.net.ssl.SSLEngine;
import org.jboss.netty.handler.ssl.SslHandler;
/*
Handler of the incoming connections
Receives the request, transforms to HTTP 1.1 if needed, establishes an outgoing connection to OS
and requests data from the OS. OS's response is unzipped if necessary and is forwarded to the client.
There will be a redirect if OS sends response 301 or 302.
*/
@ChannelPipelineCoverage("one")
public class ProxyServerHandler extends SimpleChannelUpstreamHandler {
private volatile Channel proxyServerChannel = null;
private Executor threadPool = null;
private String remoteHost = null;
private int remotePort = 80; // default value
private RemoteHostHandler remoteHostHandler = null;
private static AtomicLong requestCount = new AtomicLong(0);
public ProxyServerHandler(Executor threadPool) {
this.threadPool = threadPool;
}
public ProxyServerHandler(Executor threadPool, HttpHost destHost) {
this.threadPool = threadPool;
this.remoteHost = destHost.getHost();
this.remotePort = destHost.getPort();
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
HttpRequest request = (HttpRequest) e.getMessage();
long currRequestValue = requestCount.incrementAndGet();
if (currRequestValue % 100 == 0) {
// reporting every 100th request
System.out.println("Num Of HTTP Requests: " + currRequestValue);
}
// keep alive not supported
request.removeHeader("Proxy-Connection");
request.setHeader("Connection", "close");
if (request.getProtocolVersion() == HttpVersion.HTTP_1_0) {
request = convertRequestTo1_1(request);
}
// here we can process the request, e.g. detect specific URI and respond using
// writeResponseAndClose(ErrorResponse.create("unsupported uri")); and stop the processing
ClientBootstrap bootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(threadPool, threadPool));
ChannelPipeline p = bootstrap.getPipeline();
// Add SSL handler first to encrypt and decrypt everything.
// In this example, we use a bogus certificate in the server side
// and accept any invalid certificates in the client side.
// You will need something more complicated to identify both
// and server in the real world
SSLEngine engine = HttpsMapperSslContextFactory.getClientContext().createSSLEngine();
engine.setUseClientMode(true);
// this one provides coding / encoding
p.addLast("ssl", new SslHandler(engine));
p.addLast("decoder", new HttpResponseDecoder());
p.addLast("aggregator", new HttpChunkAggregator(1048576));
p.addLast("encoder", new HttpRequestEncoder());
remoteHostHandler = new RemoteHostHandler(request); // depends on the client's request
p.addLast("handler", remoteHostHandler);
proxyServerChannel = e.getChannel();
HttpHost proxy = HttpsMapper.getUpstreamProxy(); // currently returns null, always
InetSocketAddress dest = proxy == null ?
new InetSocketAddress(remoteHost, remotePort) :
new InetSocketAddress(proxy.getHost(), proxy.getPort());
ChannelFuture connectFuture = bootstrap.connect(dest);
connectFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
future.getChannel().getCloseFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
writeResponseAndClose(remoteHostHandler.getResponse()); // here we get the response from OS and forward it to the client
}
});
} else {
future.getChannel().close();
writeResponseAndClose(createErrorResponse("Could not connect to " + remoteHost + ":" + remotePort));
}
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("Unexpected exception from proxy server handler: " + e.getCause());
e.getChannel().close();
}
// sends the generated response into incoming connection (channel between HttpsMapper and the client)
private void writeResponseAndClose(HttpResponse response) {
if (response != null) {
response.setHeader("Connection", "close");
proxyServerChannel.write(response).addListener(ChannelFutureListener.CLOSE);
} else {
proxyServerChannel.close();
}
}
// converts HTTP 1.0 requests into HTTP 1.1
private HttpRequest convertRequestTo1_1(HttpRequest request) throws URISyntaxException {
DefaultHttpRequest newReq = new DefaultHttpRequest(HttpVersion.HTTP_1_1, request.getMethod(), request.getUri());
if (!request.getHeaderNames().isEmpty()) {
for (String name : request.getHeaderNames()) {
newReq.setHeader(name, request.getHeaders(name));
}
}
if (!newReq.containsHeader(HttpHeaders.Names.HOST)) {
URI url = new URI(newReq.getUri());
String host = url.getHost();
if (url.getPort() != -1) {
host += ":" + url.getPort();
}
newReq.setHeader(HttpHeaders.Names.HOST, host);
}
newReq.setContent(request.getContent());
return newReq;
}
// creates an http response with a specified text
public static HttpResponse createErrorResponse(String errorText) {
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
response.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=utf-8");
ChannelBuffer buf = ChannelBuffers.copiedBuffer("<html><body><h3>" + errorText + "</h3></body></html>", "utf-8");
response.setContent(buf);
response.setHeader("Content-Length", String.valueOf(buf.readableBytes()));
return response;
}
}
Код хэндлера, который общается с OS (origin server):
package ru.outofrange.sslmapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.zip.GZIPInputStream;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.ChannelStateEvent;
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.handler.codec.http.HttpHeaders;
// this class handles outgoing connections to OS
@ChannelPipelineCoverage("one")
public class RemoteHostHandler extends SimpleChannelUpstreamHandler {
private HttpRequest request = null;
private HttpResponse response = null;
public RemoteHostHandler(HttpRequest request) {
this.request = request;
}
public HttpResponse getResponse() {
return response;
}
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
// Connection to OS established, forwarding the client's request into it
e.getChannel().write(request);
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
response = (HttpResponse) e.getMessage();
// Got response from OS, unzipping if necessary
List<String> encodings = response.getHeaders(HttpHeaders.Names.CONTENT_ENCODING);
if (response.getStatus().equals(HttpResponseStatus.OK) &&
!encodings.isEmpty() && encodings.contains(HttpHeaders.Values.GZIP)) {
ChannelBuffer in = response.getContent();
int compressedLength = in.readableBytes();
GZIPInputStream gzip = new GZIPInputStream(
new ByteArrayInputStream(in.toByteBuffer().array(), 0, compressedLength));
ChannelBuffer out = ChannelBuffers.dynamicBuffer();
byte[] buf = new byte[1024];
int read;
try {
while ((read = gzip.read(buf)) > 0) {
out.writeBytes(buf, 0, read);
}
} catch (IOException ex) {}
in.clear();
response.setContent(out);
encodings.remove(HttpHeaders.Values.GZIP);
if (encodings.isEmpty()) {
response.removeHeader(HttpHeaders.Names.CONTENT_ENCODING);
}
response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(out.readableBytes()));
}
// we can process the http response here, say inject some html markup
// logging
System.err.println("");
System.err.println(request);
System.err.println("");
System.err.println(response);
System.err.println("");
e.getChannel().close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("Unexpected exception from remote host handler: " + e.getCause());
response = ProxyServerHandler.createErrorResponse("Error while processing request");
e.getChannel().close();
}
} |
package ru.outofrange.sslmapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.zip.GZIPInputStream;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.ChannelStateEvent;
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.handler.codec.http.HttpHeaders;
// this class handles outgoing connections to OS
@ChannelPipelineCoverage("one")
public class RemoteHostHandler extends SimpleChannelUpstreamHandler {
private HttpRequest request = null;
private HttpResponse response = null;
public RemoteHostHandler(HttpRequest request) {
this.request = request;
}
public HttpResponse getResponse() {
return response;
}
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
// Connection to OS established, forwarding the client's request into it
e.getChannel().write(request);
}
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
response = (HttpResponse) e.getMessage();
// Got response from OS, unzipping if necessary
List<String> encodings = response.getHeaders(HttpHeaders.Names.CONTENT_ENCODING);
if (response.getStatus().equals(HttpResponseStatus.OK) &&
!encodings.isEmpty() && encodings.contains(HttpHeaders.Values.GZIP)) {
ChannelBuffer in = response.getContent();
int compressedLength = in.readableBytes();
GZIPInputStream gzip = new GZIPInputStream(
new ByteArrayInputStream(in.toByteBuffer().array(), 0, compressedLength));
ChannelBuffer out = ChannelBuffers.dynamicBuffer();
byte[] buf = new byte[1024];
int read;
try {
while ((read = gzip.read(buf)) > 0) {
out.writeBytes(buf, 0, read);
}
} catch (IOException ex) {}
in.clear();
response.setContent(out);
encodings.remove(HttpHeaders.Values.GZIP);
if (encodings.isEmpty()) {
response.removeHeader(HttpHeaders.Names.CONTENT_ENCODING);
}
response.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String.valueOf(out.readableBytes()));
}
// we can process the http response here, say inject some html markup
// logging
System.err.println("");
System.err.println(request);
System.err.println("");
System.err.println(response);
System.err.println("");
e.getChannel().close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
System.out.println("Unexpected exception from remote host handler: " + e.getCause());
response = ProxyServerHandler.createErrorResponse("Error while processing request");
e.getChannel().close();
}
}
Классы HttpsMapperSslContextFactory и HttpsMapperTrustManagerFactory выполняют работу по шифрованию/дешифрованию:
package ru.outofrange.sslmapper;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactorySpi;
import javax.net.ssl.X509TrustManager;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.X509Certificate;
public class HttpsMapperTrustManagerFactory extends TrustManagerFactorySpi {
private static final TrustManager DUMMY_TRUST_MANAGER = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
// Always trust - it is an example.
// You should do something in the real world.
// You will reach here only if you enabled client certificate auth,
// as described in SecureChatSslContextFactory.
// System.err.println(
// "UNKNOWN CLIENT CERTIFICATE: " + chain[0].getSubjectDN());
// need to override this method, but will live it empty - we don't use it since the app acts only as SSL client
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// Always trust - it is an example.
// You should do something in the real world.
System.err.println(
"Got Server Certificate: " + chain[0].getSubjectDN());
}
};
public static TrustManager[] getTrustManagers() {
return new TrustManager[] { DUMMY_TRUST_MANAGER };
}
@Override
protected TrustManager[] engineGetTrustManagers() {
return getTrustManagers();
}
@Override
protected void engineInit(KeyStore keystore) throws KeyStoreException {
// Unused
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
throws InvalidAlgorithmParameterException {
// Unused
}
} |
package ru.outofrange.sslmapper;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactorySpi;
import javax.net.ssl.X509TrustManager;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.X509Certificate;
public class HttpsMapperTrustManagerFactory extends TrustManagerFactorySpi {
private static final TrustManager DUMMY_TRUST_MANAGER = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
// Always trust - it is an example.
// You should do something in the real world.
// You will reach here only if you enabled client certificate auth,
// as described in SecureChatSslContextFactory.
// System.err.println(
// "UNKNOWN CLIENT CERTIFICATE: " + chain[0].getSubjectDN());
// need to override this method, but will live it empty - we don't use it since the app acts only as SSL client
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// Always trust - it is an example.
// You should do something in the real world.
System.err.println(
"Got Server Certificate: " + chain[0].getSubjectDN());
}
};
public static TrustManager[] getTrustManagers() {
return new TrustManager[] { DUMMY_TRUST_MANAGER };
}
@Override
protected TrustManager[] engineGetTrustManagers() {
return getTrustManagers();
}
@Override
protected void engineInit(KeyStore keystore) throws KeyStoreException {
// Unused
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
throws InvalidAlgorithmParameterException {
// Unused
}
}
public final class HttpsMapperSslContextFactory {
private static final String PROTOCOL = "TLS";
private static final SSLContext CLIENT_CONTEXT;
static {
SSLContext clientContext;
try {
clientContext = SSLContext.getInstance(PROTOCOL);
clientContext.init(null, HttpsMapperTrustManagerFactory.getTrustManagers(), null);
} catch (Exception e) {
throw new Error(
"Failed to initialize the client-side SSLContext", e);
}
CLIENT_CONTEXT = clientContext;
}
public static SSLContext getClientContext() {
return CLIENT_CONTEXT;
}
private HttpsMapperSslContextFactory() {
// Unused
}
} |
public final class HttpsMapperSslContextFactory {
private static final String PROTOCOL = "TLS";
private static final SSLContext CLIENT_CONTEXT;
static {
SSLContext clientContext;
try {
clientContext = SSLContext.getInstance(PROTOCOL);
clientContext.init(null, HttpsMapperTrustManagerFactory.getTrustManagers(), null);
} catch (Exception e) {
throw new Error(
"Failed to initialize the client-side SSLContext", e);
}
CLIENT_CONTEXT = clientContext;
}
public static SSLContext getClientContext() {
return CLIENT_CONTEXT;
}
private HttpsMapperSslContextFactory() {
// Unused
}
}
Вспомогательный класс для передачи данных:
public class HttpHost {
private String host;
private int port;
public HttpHost(String host, int port) {
this.host = host;
this.port = port;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
} |
public class HttpHost {
private String host;
private int port;
public HttpHost(String host, int port) {
this.host = host;
this.port = port;
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
Приложение тестировалось на сайте eff.org (в дополнение к обязательному тестированию на облаке Амазона). Подходящий сайт оказалось найти не так просто, многие присылают в ответ код редиректа.
Пример строки для запуска:
java -jar server-0.1-jar-with-dependencies.jar 9026 eff.org 443 |
java -jar server-0.1-jar-with-dependencies.jar 9026 eff.org 443
В данном случае мы указываем, что приложение должно запуститься на порте 9026 и отображать запросы на него в хост eff.org на порт 443. В случае с eff.org можно было тестировать прямо в браузере.
Приложение показало себя полностью работоспособным при порядочной нагрузке.
Полезные ссылки:
http://seeallhearall.blogspot.ru/2012/05/netty-tutorial-part-1-introduction-to.html http://seeallhearall.blogspot.ru/2012/06/netty-tutorial-part-15-on-channel.html
[sc:social_networks ]