Skip to content

Commit

Permalink
forwarding rules, lazy host matching
Browse files Browse the repository at this point in the history
  • Loading branch information
danthe1st committed Mar 8, 2024
1 parent 2433397 commit fead8fc
Show file tree
Hide file tree
Showing 19 changed files with 411 additions and 103 deletions.
12 changes: 11 additions & 1 deletion graal/reflect-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@
"allPublicConstructors" : true,
"allRecordComponents": true
},{
"name" : "io.github.danthe1st.httpsintercept.config.HostMatcher",
"name" : "io.github.danthe1st.httpsintercept.config.HostMatcherConfig",
"allPublicConstructors" : true,
"allRecordComponents": true
},{
"name" : "io.github.danthe1st.httpsintercept.rules.SetHeaderRule",
"allPublicConstructors" : true,
"allRecordComponents": true
},{
"name" : "java.util.HashSet",
"allPublicConstructors" : true
},{
"name" : "java.util.ArrayList",
"allPublicConstructors" : true
},{
"name" : "java.util.Set",
"allPublicMethods": true
},{
"name" : "java.util.List",
"allPublicMethods": true
}
]
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<version>5.10.2</version>
<scope>test</scope>
</dependency>

</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import io.github.danthe1st.httpsintercept.rules.PreForwardRule;

public record Config(
HostMatcher ignoredHosts
HostMatcherConfig ignoredHosts,
List<PreForwardRule> preForwardRules
) {

public Config {
public Config(HostMatcherConfig ignoredHosts, List<PreForwardRule> preForwardRules) {
Objects.requireNonNull(ignoredHosts);
this.ignoredHosts = ignoredHosts;
this.preForwardRules = List.copyOf(preForwardRules);
}

public static Config load(Path path) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@
import java.util.Set;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public record HostMatcher(
Set<String> exactHosts,
Set<String> hostParts,
Set<Pattern> hostRegexes) {

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

public HostMatcher(Set<String> exactHosts, Set<String> hostParts, Set<Pattern> hostRegexes) {
this.exactHosts = copyOrEmptyIfNull(exactHosts);
this.hostParts = copyOrEmptyIfNull(hostParts);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.danthe1st.httpsintercept.config;

import java.util.Objects;
import java.util.Set;

public record HostMatcherConfig(Set<String> exactHosts,
Set<String> hostParts,
Set<String> hostRegexes) {
public HostMatcherConfig {
Objects.requireNonNull(exactHosts);
Objects.requireNonNull(hostParts);
Objects.requireNonNull(hostRegexes);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import io.github.danthe1st.httpsintercept.config.Config;
import io.github.danthe1st.httpsintercept.config.HostMatcherConfig;
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.github.danthe1st.httpsintercept.matcher.IterativeHostMatcher;
import io.github.danthe1st.httpsintercept.rules.PreForwardRule;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
Expand All @@ -27,10 +33,19 @@ public class ServerHandlersInit extends ChannelInitializer<SocketChannel> {
private final SNIHandlerMapping sniMapping;
private final Config config;

private IterativeHostMatcher<PreForwardRule> preForwardMatcher;

public ServerHandlersInit(Bootstrap clientBootstrap, Config config) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException {
this.clientBootstrapTemplate = clientBootstrap;
sniMapping = SNIHandlerMapping.createMapping();
this.config = config;

List<PreForwardRule> preForwardRules = config.preForwardRules();
List<Map.Entry<HostMatcherConfig, PreForwardRule>> rules = new ArrayList<>();
for(PreForwardRule preForwardRule : preForwardRules){
rules.add(Map.entry(preForwardRule.hostMatcher(), preForwardRule));
}
preForwardMatcher = new IterativeHostMatcher<>(rules);
}

@Override
Expand All @@ -40,7 +55,7 @@ protected void initChannel(SocketChannel socketChannel) throws Exception {
sniHandler,
new HttpServerCodec(),
new HttpObjectAggregator(1048576),
new IncomingHttpRequestHandler(sniHandler, clientBootstrapTemplate)
new IncomingHttpRequestHandler(sniHandler, clientBootstrapTemplate, preForwardMatcher)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.io.IOException;
import java.io.PrintStream;

import io.github.danthe1st.httpsintercept.matcher.IterativeHostMatcher;
import io.github.danthe1st.httpsintercept.rules.PreForwardRule;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
Expand All @@ -28,15 +30,17 @@ public final class IncomingHttpRequestHandler extends SimpleChannelInboundHandle

private final SniHandler sniHandler;
private final Bootstrap clientBootstrap;
private final IterativeHostMatcher<PreForwardRule> hostMatcher;

/**
* @param sniHandler Netty handler for Server Name Identification (contains the actual target host name)
* @param clientSslContext {@link SslContext} used for the outgoing request
* @param clientBootstrap template for sending the outgoing request
*/
public IncomingHttpRequestHandler(SniHandler sniHandler, Bootstrap clientBootstrap) {
public IncomingHttpRequestHandler(SniHandler sniHandler, Bootstrap clientBootstrap, IterativeHostMatcher<PreForwardRule> hostMatcher) {
this.sniHandler = sniHandler;
this.clientBootstrap = clientBootstrap;
this.hostMatcher = hostMatcher;
}

@Override
Expand All @@ -56,6 +60,13 @@ private void logRequest(FullHttpRequest fullHttpRequest) {

private void forwardRequest(ChannelHandlerContext channelHandlerContext, FullHttpRequest fullHttpRequest) throws InterruptedException, IOException {
String hostname = sniHandler.hostname();

for(PreForwardRule preForwardRule : hostMatcher.matchesAsIterable(hostname)){
if(!preForwardRule.processRequest(fullHttpRequest, channelHandlerContext.channel())){
return;
}
}

Bootstrap actualClientBootstrap = clientBootstrap.clone()
.handler(new OutgoingHttpRequestHandler(channelHandlerContext, hostname));
try{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.LastHttpContent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Handles the response of the outgoing/forwarded request
*/
final class ResponseHandler extends ChannelInboundHandlerAdapter {

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

private final ChannelHandlerContext originalClientContext;

public ResponseHandler(ChannelHandlerContext originalClientContext) {
Expand All @@ -17,19 +21,19 @@ public ResponseHandler(ChannelHandlerContext originalClientContext) {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
OutgoingHttpRequestHandler.LOG.debug("read: {}", msg);
LOG.debug("read: {}", msg);
originalClientContext.writeAndFlush(msg);

if(msg instanceof LastHttpContent){
OutgoingHttpRequestHandler.LOG.debug("last HTTP content");
LOG.debug("last HTTP content");
originalClientContext.channel().close();
ctx.channel().close();
}
}

@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
OutgoingHttpRequestHandler.LOG.debug("channel unregistered");
LOG.debug("channel unregistered");
originalClientContext.channel().close();
ctx.channel().close();
super.channelUnregistered(ctx);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
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.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.github.danthe1st.httpsintercept.config.Config;
import io.github.danthe1st.httpsintercept.config.HostMatcher;
import io.github.danthe1st.httpsintercept.handler.raw.RawForwardIncomingRequestHandler;
import io.github.danthe1st.httpsintercept.matcher.IterativeHostMatcher;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
Expand All @@ -29,30 +24,19 @@ public class CustomSniHandler extends SniHandler {

private final Bootstrap clientBootstrapTemplate;

private final HostMatcher ignoredHosts;
private final IterativeHostMatcher<Object> ignoredHosts;

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

ignoredHosts = config.ignoredHosts();
ignoredHosts = new IterativeHostMatcher<>(List.of(Map.entry(config.ignoredHosts(), new Object())));
}

private static Set<String> loadIgnoredHosts() 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.matches(hostname)){
// TODO no hostname
if(ignoredHosts.allMatches(hostname).hasNext()){
LOG.info("skipping hostname {}", hostname);

boolean foundThis = false;
Expand All @@ -75,9 +59,4 @@ protected void replaceHandler(ChannelHandlerContext channelHandlerContext, Strin
super.replaceHandler(channelHandlerContext, hostname, sslContext);
}
}

@Override
public String hostname() {
return super.hostname();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.github.danthe1st.httpsintercept.matcher;

import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.Predicate;

final class FilterIterator<T> implements Iterator<T> {
private final Iterator<T> iterator;
private final Predicate<T> filter;
private T current;

public FilterIterator(Iterator<T> iterator, Predicate<T> filter) {
this.iterator = iterator;
this.filter = filter;
}
@Override
public boolean hasNext() {
if(current != null){
return true;
}
while(iterator.hasNext()){
T next = iterator.next();
if(filter.test(next)){
current = next;
return true;
}
}
return false;
}

@Override
public T next() {
if(!hasNext()){
throw new NoSuchElementException();
}
T ret = current;
current = null;
return ret;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.github.danthe1st.httpsintercept.matcher;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

final class HostPartIterator<T> extends IteratingIterator<T> {
private final String hostname;
private int index = 0;
private Iterator<T> current = Collections.emptyIterator();
private final Map<String, List<T>> hostParts;

HostPartIterator(String hostname, Map<String, List<T>> hostParts) {
this.hostname = hostname;
this.hostParts = hostParts;
}

@Override
protected Iterator<T> findNextIterator() {
if(current.hasNext()){
return current;
}
do{
String hostPart = hostname.substring(index);
if(hostParts.containsKey(hostPart)){
current = hostParts.get(hostPart).iterator();
if(current.hasNext()){
return current;
}
}
}while((index = hostname.indexOf('.', index) + 1) != 0 && index < hostname.length());
return Collections.emptyIterator();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.github.danthe1st.httpsintercept.matcher;

import java.util.Iterator;

abstract class IteratingIterator<T> implements Iterator<T> {

protected abstract Iterator<T> findNextIterator();

@Override
public boolean hasNext() {
return findNextIterator().hasNext();
}

@Override
public T next() {
return findNextIterator().next();
}
}
Loading

0 comments on commit fead8fc

Please sign in to comment.