io.netty.handler.proxy.HttpProxyHandler Maven / Gradle / Ivy
/*
* 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:
*
* https://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.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.base64.Base64;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.DefaultHttpHeadersFactory;
import io.netty.handler.codec.http.HttpHeadersFactory;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.ObjectUtil;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
/**
* Handler that establishes a blind forwarding proxy tunnel using
* HTTP/1.1 CONNECT request. It can be used to
* establish plaintext or secure tunnels.
*
* HTTP users who need to connect to a
* message-forwarding HTTP proxy agent instead of a
* tunneling proxy should not use this handler.
*/
public final class HttpProxyHandler extends ProxyHandler {
private static final String PROTOCOL = "http";
private static final String AUTH_BASIC = "basic";
// Wrapper for the HttpClientCodec to prevent it to be removed by other handlers by mistake (for example the
// WebSocket*Handshaker.
//
// See:
// - https://github.com/netty/netty/issues/5201
// - https://github.com/netty/netty/issues/5070
private final HttpClientCodecWrapper codecWrapper = new HttpClientCodecWrapper();
private final String username;
private final String password;
private final CharSequence authorization;
private final HttpHeaders outboundHeaders;
private final boolean ignoreDefaultPortsInConnectHostHeader;
private HttpResponseStatus status;
private HttpHeaders inboundHeaders;
public HttpProxyHandler(SocketAddress proxyAddress) {
this(proxyAddress, null);
}
public HttpProxyHandler(SocketAddress proxyAddress, HttpHeaders headers) {
this(proxyAddress, headers, false);
}
public HttpProxyHandler(SocketAddress proxyAddress,
HttpHeaders headers,
boolean ignoreDefaultPortsInConnectHostHeader) {
super(proxyAddress);
username = null;
password = null;
authorization = null;
this.outboundHeaders = headers;
this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
}
public HttpProxyHandler(SocketAddress proxyAddress, String username, String password) {
this(proxyAddress, username, password, null);
}
public HttpProxyHandler(SocketAddress proxyAddress, String username, String password,
HttpHeaders headers) {
this(proxyAddress, username, password, headers, false);
}
public HttpProxyHandler(SocketAddress proxyAddress,
String username,
String password,
HttpHeaders headers,
boolean ignoreDefaultPortsInConnectHostHeader) {
super(proxyAddress);
this.username = ObjectUtil.checkNotNull(username, "username");
this.password = ObjectUtil.checkNotNull(password, "password");
ByteBuf authz = Unpooled.copiedBuffer(username + ':' + password, CharsetUtil.UTF_8);
ByteBuf authzBase64;
try {
authzBase64 = Base64.encode(authz, false);
} finally {
authz.release();
}
try {
authorization = new AsciiString("Basic " + authzBase64.toString(CharsetUtil.US_ASCII));
} finally {
authzBase64.release();
}
this.outboundHeaders = headers;
this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
}
@Override
public String protocol() {
return PROTOCOL;
}
@Override
public String authScheme() {
return authorization != null? AUTH_BASIC : AUTH_NONE;
}
public String username() {
return username;
}
public String password() {
return password;
}
@Override
protected void addCodec(ChannelHandlerContext ctx) throws Exception {
ChannelPipeline p = ctx.pipeline();
String name = ctx.name();
p.addBefore(name, null, codecWrapper);
}
@Override
protected void removeEncoder(ChannelHandlerContext ctx) throws Exception {
codecWrapper.codec.removeOutboundHandler();
}
@Override
protected void removeDecoder(ChannelHandlerContext ctx) throws Exception {
codecWrapper.codec.removeInboundHandler();
}
@Override
protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception {
InetSocketAddress raddr = destinationAddress();
String hostString = HttpUtil.formatHostnameForHttp(raddr);
int port = raddr.getPort();
String url = hostString + ":" + port;
String hostHeader = (ignoreDefaultPortsInConnectHostHeader && (port == 80 || port == 443)) ?
hostString :
url;
HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory().withValidation(false);
FullHttpRequest req = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1, HttpMethod.CONNECT,
url,
Unpooled.EMPTY_BUFFER, headersFactory, headersFactory);
req.headers().set(HttpHeaderNames.HOST, hostHeader);
if (authorization != null) {
req.headers().set(HttpHeaderNames.PROXY_AUTHORIZATION, authorization);
}
if (outboundHeaders != null) {
req.headers().add(outboundHeaders);
}
return req;
}
@Override
protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception {
if (response instanceof HttpResponse) {
if (status != null) {
throw new HttpProxyConnectException(exceptionMessage("too many responses"), /*headers=*/ null);
}
HttpResponse res = (HttpResponse) response;
status = res.status();
inboundHeaders = res.headers();
}
boolean finished = response instanceof LastHttpContent;
if (finished) {
if (status == null) {
throw new HttpProxyConnectException(exceptionMessage("missing response"), inboundHeaders);
}
if (status.code() != 200) {
throw new HttpProxyConnectException(exceptionMessage("status: " + status), inboundHeaders);
}
}
return finished;
}
/**
* Specific case of a connection failure, which may include headers from the proxy.
*/
public static final class HttpProxyConnectException extends ProxyConnectException {
private static final long serialVersionUID = -8824334609292146066L;
private final HttpHeaders headers;
/**
* @param message The failure message.
* @param headers Header associated with the connection failure. May be {@code null}.
*/
public HttpProxyConnectException(String message, HttpHeaders headers) {
super(message);
this.headers = headers;
}
/**
* Returns headers, if any. May be {@code null}.
*/
public HttpHeaders headers() {
return headers;
}
}
private static final class HttpClientCodecWrapper implements ChannelInboundHandler, ChannelOutboundHandler {
final HttpClientCodec codec = new HttpClientCodec();
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
codec.handlerAdded(ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
codec.handlerRemoved(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
codec.exceptionCaught(ctx, cause);
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
codec.channelRegistered(ctx);
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
codec.channelUnregistered(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
codec.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
codec.channelInactive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
codec.channelRead(ctx, msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
codec.channelReadComplete(ctx);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
codec.userEventTriggered(ctx, evt);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
codec.channelWritabilityChanged(ctx);
}
@Override
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
codec.bind(ctx, localAddress, promise);
}
@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
codec.connect(ctx, remoteAddress, localAddress, promise);
}
@Override
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
codec.disconnect(ctx, promise);
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
codec.close(ctx, promise);
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
codec.deregister(ctx, promise);
}
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
codec.read(ctx);
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
codec.write(ctx, msg, promise);
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
codec.flush(ctx);
}
}
}