Skip to content

Commit

Permalink
allow ignoring hosts
Browse files Browse the repository at this point in the history
  • Loading branch information
danthe1st committed Feb 28, 2024
1 parent 1116e19 commit 036b041
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 23 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.crt
*.pem
.secret
ignoredHosts.txt

# Created by https://www.toptal.com/developers/gitignore/api/eclipse,maven,java,intellij+all
# Edit at https://www.toptal.com/developers/gitignore?templates=eclipse,maven,java,intellij+all
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

import io.github.danthe1st.httpsintercept.handler.ServerHandlersInit;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelOption;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package io.github.danthe1st.httpsintercept;
package io.github.danthe1st.httpsintercept.handler;

import java.io.IOException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

import io.github.danthe1st.httpsintercept.handler.http.IncomingHttpRequestHandler;
import io.github.danthe1st.httpsintercept.handler.sni.CustomSniHandler;
import io.github.danthe1st.httpsintercept.handler.sni.SNIHandlerMapping;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
Expand All @@ -20,24 +23,23 @@ public class ServerHandlersInit extends ChannelInitializer<SocketChannel> {

static final Logger LOG = LoggerFactory.getLogger(ServerHandlersInit.class);

private final Bootstrap clientBootstrap;
private final Bootstrap clientBootstrapTemplate;
private final SslContext clientSslContext;
private final SSLHandlerMapping sniMapping;
private final SNIHandlerMapping sniMapping;

public ServerHandlersInit(Bootstrap clientBootstrap) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
this.clientBootstrap = clientBootstrap;
sniMapping = new SSLHandlerMapping();
this.clientBootstrapTemplate = clientBootstrap;
sniMapping = new SNIHandlerMapping();
clientSslContext = SslContextBuilder.forClient().build();
}

@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
SniHandler sniHandler = new SniHandler(sniMapping);
SniHandler sniHandler = new CustomSniHandler(sniMapping, clientBootstrapTemplate);
socketChannel.pipeline().addLast(
sniHandler,
new HttpServerCodec(),
new HttpObjectAggregator(1048576),
new HttpRequestHandler(sniHandler, clientSslContext, clientBootstrap)
new IncomingHttpRequestHandler(sniHandler, clientSslContext, clientBootstrapTemplate)
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.danthe1st.httpsintercept;
package io.github.danthe1st.httpsintercept.handler.http;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
Expand All @@ -22,8 +22,8 @@
/**
* Forwards incoming (already decrypted and preprocessed) HTTPs requests to the requested server and sends the response back
*/
final class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
static final Logger LOG = LoggerFactory.getLogger(HttpRequestHandler.class);
public final class IncomingHttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
static final Logger LOG = LoggerFactory.getLogger(IncomingHttpRequestHandler.class);

private final SniHandler sniHandler;
private final Bootstrap clientBootstrap;
Expand All @@ -34,7 +34,7 @@ final class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpReque
* @param clientSslContext {@link SslContext} used for the outgoing request
* @param clientBootstrap template for sending the outgoing request
*/
HttpRequestHandler(SniHandler sniHandler, SslContext clientSslContext, Bootstrap clientBootstrap) {
public IncomingHttpRequestHandler(SniHandler sniHandler, SslContext clientSslContext, Bootstrap clientBootstrap) {
this.sniHandler = sniHandler;
this.clientBootstrap = clientBootstrap;
this.clientSslContext = clientSslContext;
Expand All @@ -56,7 +56,7 @@ private void logRequest(FullHttpRequest fullHttpRequest) {

private void forwardRequest(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) throws InterruptedException {
Bootstrap actualClientBootstrap = clientBootstrap.clone()
.handler(new ForwardedRequestHandler(channelHandlerContext, clientSslContext));
.handler(new OutgoingHttpRequestHandler(channelHandlerContext, clientSslContext));

Channel outChannel = actualClientBootstrap.connect(sniHandler.hostname(), 443)
.sync()
Expand All @@ -71,7 +71,7 @@ private void forwardRequest(ChannelHandlerContext channelHandlerContext, FullHtt
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if(!(cause instanceof IllegalReferenceCountException)){
ServerHandlersInit.LOG.error("An exception occured trying to process a request", cause);
LOG.error("An exception occured trying to process a request", cause);
Channel channel = ctx.channel();
try(ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream exceptionStream = new PrintStream(baos)){
Expand All @@ -88,6 +88,5 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E
channel.close();
}
}

}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.danthe1st.httpsintercept;
package io.github.danthe1st.httpsintercept.handler.http;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
Expand All @@ -15,13 +15,13 @@
* Requests need to be forwarded to the requested server.
* This class encodes the forwarded request, decodes the response and sends it back
*/
final class ForwardedRequestHandler extends ChannelInitializer<SocketChannel> {
private static final Logger LOG = LoggerFactory.getLogger(ForwardedRequestHandler.class);
final class OutgoingHttpRequestHandler extends ChannelInitializer<SocketChannel> {
private static final Logger LOG = LoggerFactory.getLogger(OutgoingHttpRequestHandler.class);

private final ChannelHandlerContext originalClientContext;
private final SslContext forwardSslContext;

ForwardedRequestHandler(ChannelHandlerContext originalClientContext, SslContext clientSslContext) {
OutgoingHttpRequestHandler(ChannelHandlerContext originalClientContext, SslContext clientSslContext) {
this.originalClientContext = originalClientContext;
this.forwardSslContext = clientSslContext;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.github.danthe1st.httpsintercept.handler.raw;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Handler for requests that need to be forwarded to the requested server.
* This class accepts raw SSL/TLS requests that should be forwarded without decoding/re-encoding.
* It sends a request to the target server which is then processed by {@link RawForwardedOutgoingRequestHandler}.
*/
public final class RawForwardIncomingRequestHandler extends ChannelInboundHandlerAdapter {

private static final Logger LOG = LoggerFactory.getLogger(RawForwardIncomingRequestHandler.class);

private final String hostname;
private final Bootstrap clientBootstrapTemplate;
private Channel outChannel = null;

public RawForwardIncomingRequestHandler(String hostname, Bootstrap clientBootstrapTemplate) {
this.hostname = hostname;
this.clientBootstrapTemplate = clientBootstrapTemplate;
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws InterruptedException {
if(outChannel == null){
Bootstrap actualClientBootstrap = clientBootstrapTemplate.clone()
.handler(new RawForwardedOutgoingRequestHandler(ctx));
try{
outChannel = actualClientBootstrap.connect(hostname, 443)
.sync()
.channel();
}catch(RuntimeException e){
// this can happen due to internet connection issues or something on the remove side
// cannot do much more than aborting the connection
// because this is before a TLS connection is established (when the client hello packet is read)
LOG.error("An exception occured trying to establish a raw connection for forwarding", e);
ctx.channel().close();
return;
}
}

outChannel.writeAndFlush(msg).sync();
}

@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
if(outChannel != null){
outChannel.close();
}
ctx.channel().close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.github.danthe1st.httpsintercept.handler.raw;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Handler for requests that are forwarded to the requested server without decoding/re-encoding.
* @see RawForwardIncomingRequestHandler
*/
public final class RawForwardedOutgoingRequestHandler extends ChannelInitializer<SocketChannel> {
private static final Logger LOG = LoggerFactory.getLogger(RawForwardedOutgoingRequestHandler.class);

private final ChannelHandlerContext originalClientContext;

public RawForwardedOutgoingRequestHandler(ChannelHandlerContext originalClientContext) {
this.originalClientContext = originalClientContext;
}

@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new ResponseHandler());
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
LOG.error("An exception occured while forwarding a request", cause);
originalClientContext.channel().close();
ctx.channel().close();
}

@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
LOG.info("channel unregistered");
}

private final class ResponseHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
LOG.debug("read: {}", msg);
originalClientContext.writeAndFlush(msg);
}

@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
LOG.info("channel unregistered");
originalClientContext.channel().close();
ctx.channel().close();
super.channelUnregistered(ctx);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
LOG.error("An exception occured while forwarding a raw request", cause);
originalClientContext.channel().close();
ctx.channel().close();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.github.danthe1st.httpsintercept.handler.sni;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.github.danthe1st.httpsintercept.handler.raw.RawForwardIncomingRequestHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.util.Mapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CustomSniHandler extends SniHandler {

private static final Logger LOG = LoggerFactory.getLogger(CustomSniHandler.class);

private final Bootstrap clientBootstrapTemplate;

private final Set<String> ignoredHosts;

public CustomSniHandler(Mapping<? super String, ? extends SslContext> mapping, Bootstrap clientBootstrapTemplate) throws IOException {
super(mapping);
this.clientBootstrapTemplate = clientBootstrapTemplate;

ignoredHosts = getIgnoredHosts();
}

private Set<String> getIgnoredHosts() throws IOException {
Path ignoredHostsPath = Path.of("ignoredHosts.txt");
if(!Files.exists(ignoredHostsPath)){
Files.createFile(ignoredHostsPath);
return Collections.emptySet();
}
try(Stream<String> ignoredHostStream = Files.lines(ignoredHostsPath)){
return ignoredHostStream.collect(Collectors.toSet());
}
}

@Override
protected void replaceHandler(ChannelHandlerContext channelHandlerContext, String hostname, SslContext sslContext) throws Exception {
ChannelPipeline pipeline = channelHandlerContext.pipeline();
if(ignoredHosts.contains(hostname)){
LOG.debug("skipping hostname {}", hostname);

boolean foundThis = false;

for(Iterator<Map.Entry<String, ChannelHandler>> it = pipeline.iterator();it.hasNext();) {
ChannelHandler handler = it.next().getValue();
if(foundThis){
it.remove();
}
if(handler == this){
foundThis = true;
}
}

if(!foundThis){
throw new IllegalStateException("cannot find self handler in pipeline");
}
pipeline.replace(this, "forwardNoProcess", new RawForwardIncomingRequestHandler(hostname, clientBootstrapTemplate));
}else{
super.replaceHandler(channelHandlerContext, hostname, sslContext);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.github.danthe1st.httpsintercept;
package io.github.danthe1st.httpsintercept.handler.sni;

import java.io.IOException;
import java.io.InputStream;
Expand All @@ -16,6 +16,7 @@

import javax.net.ssl.SSLException;

import io.github.danthe1st.httpsintercept.CertificateGenerator;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.util.Mapping;
Expand All @@ -24,17 +25,17 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SSLHandlerMapping implements Mapping<String, SslContext> {
public class SNIHandlerMapping implements Mapping<String, SslContext> {

private static final Logger LOG = LoggerFactory.getLogger(SSLHandlerMapping.class);
private static final Logger LOG = LoggerFactory.getLogger(SNIHandlerMapping.class);

private static final String KEYSTORE = "interceptor.jks";


private final KeyStore ks;
private final char[] privateKeyPassword;

public SSLHandlerMapping() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
public SNIHandlerMapping() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {

Path secretFile = Path.of(".secret");
if(!Files.exists(secretFile)){
Expand Down

0 comments on commit 036b041

Please sign in to comment.