2014-08-25 10:50:37 +02:00
|
|
|
/*
|
|
|
|
* Copyright 2014 The Netty Project
|
|
|
|
*
|
|
|
|
* The Netty Project licenses this file to you under the Apache License,
|
|
|
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
|
|
|
* with the License. You may obtain a copy of the License at:
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
|
|
* License for the specific language governing permissions and limitations
|
|
|
|
* under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package io.netty.handler.proxy;
|
|
|
|
|
|
|
|
import io.netty.buffer.Unpooled;
|
|
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
|
|
import io.netty.channel.ChannelPipeline;
|
|
|
|
import io.netty.channel.socket.SocketChannel;
|
|
|
|
import io.netty.handler.codec.LineBasedFrameDecoder;
|
|
|
|
import io.netty.handler.codec.http.DefaultFullHttpResponse;
|
|
|
|
import io.netty.handler.codec.http.FullHttpRequest;
|
|
|
|
import io.netty.handler.codec.http.FullHttpResponse;
|
2014-10-31 08:48:28 +01:00
|
|
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
2014-08-25 10:50:37 +02:00
|
|
|
import io.netty.handler.codec.http.HttpMethod;
|
|
|
|
import io.netty.handler.codec.http.HttpObjectAggregator;
|
|
|
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
|
|
|
import io.netty.handler.codec.http.HttpServerCodec;
|
|
|
|
import io.netty.handler.codec.http.HttpVersion;
|
2016-12-19 21:13:43 +01:00
|
|
|
import io.netty.util.internal.SocketUtils;
|
2014-08-25 10:50:37 +02:00
|
|
|
|
|
|
|
import java.net.InetSocketAddress;
|
|
|
|
import java.net.SocketAddress;
|
2019-02-12 17:04:09 +01:00
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
import java.util.Base64;
|
2014-08-25 10:50:37 +02:00
|
|
|
|
|
|
|
import static org.hamcrest.CoreMatchers.is;
|
|
|
|
import static org.hamcrest.Matchers.*;
|
|
|
|
import static org.junit.Assert.*;
|
|
|
|
|
|
|
|
final class HttpProxyServer extends ProxyServer {
|
|
|
|
|
|
|
|
HttpProxyServer(boolean useSsl, TestMode testMode, InetSocketAddress destination) {
|
|
|
|
super(useSsl, testMode, destination);
|
|
|
|
}
|
|
|
|
|
|
|
|
HttpProxyServer(
|
|
|
|
boolean useSsl, TestMode testMode, InetSocketAddress destination, String username, String password) {
|
|
|
|
super(useSsl, testMode, destination, username, password);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void configure(SocketChannel ch) throws Exception {
|
|
|
|
ChannelPipeline p = ch.pipeline();
|
|
|
|
switch (testMode) {
|
|
|
|
case INTERMEDIARY:
|
|
|
|
p.addLast(new HttpServerCodec());
|
|
|
|
p.addLast(new HttpObjectAggregator(1));
|
|
|
|
p.addLast(new HttpIntermediaryHandler());
|
|
|
|
break;
|
|
|
|
case TERMINAL:
|
|
|
|
p.addLast(new HttpServerCodec());
|
|
|
|
p.addLast(new HttpObjectAggregator(1));
|
|
|
|
p.addLast(new HttpTerminalHandler());
|
|
|
|
break;
|
|
|
|
case UNRESPONSIVE:
|
|
|
|
p.addLast(UnresponsiveHandler.INSTANCE);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean authenticate(ChannelHandlerContext ctx, FullHttpRequest req) {
|
|
|
|
assertThat(req.method(), is(HttpMethod.CONNECT));
|
|
|
|
|
|
|
|
if (testMode != TestMode.INTERMEDIARY) {
|
|
|
|
ctx.pipeline().addBefore(ctx.name(), "lineDecoder", new LineBasedFrameDecoder(64, false, true));
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.pipeline().remove(HttpObjectAggregator.class);
|
2016-01-10 22:19:55 +01:00
|
|
|
ctx.pipeline().get(HttpServerCodec.class).removeInboundHandler();
|
2014-08-25 10:50:37 +02:00
|
|
|
|
|
|
|
boolean authzSuccess = false;
|
|
|
|
if (username != null) {
|
2014-11-20 12:39:18 +01:00
|
|
|
CharSequence authz = req.headers().get(HttpHeaderNames.PROXY_AUTHORIZATION);
|
2014-08-25 10:50:37 +02:00
|
|
|
if (authz != null) {
|
2016-08-01 11:17:05 +02:00
|
|
|
String[] authzParts = authz.toString().split(" ", 2);
|
2019-02-12 17:04:09 +01:00
|
|
|
byte[] authzCreds = Base64.getDecoder().decode(authzParts[1]);
|
2014-11-20 12:39:18 +01:00
|
|
|
|
|
|
|
String expectedAuthz = username + ':' + password;
|
|
|
|
authzSuccess = "Basic".equals(authzParts[0]) &&
|
2019-02-12 17:04:09 +01:00
|
|
|
expectedAuthz.equals(new String(authzCreds, StandardCharsets.UTF_8));
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
authzSuccess = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return authzSuccess;
|
|
|
|
}
|
|
|
|
|
|
|
|
private final class HttpIntermediaryHandler extends IntermediaryHandler {
|
|
|
|
|
|
|
|
private SocketAddress intermediaryDestination;
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected boolean handleProxyProtocol(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
|
|
FullHttpRequest req = (FullHttpRequest) msg;
|
|
|
|
FullHttpResponse res;
|
|
|
|
if (!authenticate(ctx, req)) {
|
|
|
|
res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED);
|
2014-10-31 08:48:28 +01:00
|
|
|
res.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
|
2014-08-25 10:50:37 +02:00
|
|
|
} else {
|
|
|
|
res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
|
|
|
String uri = req.uri();
|
|
|
|
int lastColonPos = uri.lastIndexOf(':');
|
|
|
|
assertThat(lastColonPos, is(greaterThan(0)));
|
2016-12-19 21:13:43 +01:00
|
|
|
intermediaryDestination = SocketUtils.socketAddress(
|
2014-08-25 10:50:37 +02:00
|
|
|
uri.substring(0, lastColonPos), Integer.parseInt(uri.substring(lastColonPos + 1)));
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.write(res);
|
2016-01-10 22:19:55 +01:00
|
|
|
ctx.pipeline().get(HttpServerCodec.class).removeOutboundHandler();
|
2014-08-25 10:50:37 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected SocketAddress intermediaryDestination() {
|
|
|
|
return intermediaryDestination;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private final class HttpTerminalHandler extends TerminalHandler {
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected boolean handleProxyProtocol(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
|
|
FullHttpRequest req = (FullHttpRequest) msg;
|
|
|
|
FullHttpResponse res;
|
|
|
|
boolean sendGreeting = false;
|
|
|
|
|
|
|
|
if (!authenticate(ctx, req)) {
|
|
|
|
res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNAUTHORIZED);
|
2014-10-31 08:48:28 +01:00
|
|
|
res.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
|
2014-08-25 10:50:37 +02:00
|
|
|
} else if (!req.uri().equals(destination.getHostString() + ':' + destination.getPort())) {
|
|
|
|
res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN);
|
2014-10-31 08:48:28 +01:00
|
|
|
res.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
|
2014-08-25 10:50:37 +02:00
|
|
|
} else {
|
|
|
|
res = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
|
|
|
sendGreeting = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.write(res);
|
2016-01-10 22:19:55 +01:00
|
|
|
ctx.pipeline().get(HttpServerCodec.class).removeOutboundHandler();
|
2014-08-25 10:50:37 +02:00
|
|
|
|
|
|
|
if (sendGreeting) {
|
2019-02-12 17:04:09 +01:00
|
|
|
ctx.write(Unpooled.copiedBuffer("0\n", StandardCharsets.US_ASCII));
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|