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;
|
|
|
|
|
2019-02-04 10:32:25 +01:00
|
|
|
import static java.util.Objects.requireNonNull;
|
|
|
|
|
2014-08-25 10:50:37 +02:00
|
|
|
import io.netty.buffer.Unpooled;
|
|
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
|
|
import io.netty.channel.ChannelPipeline;
|
|
|
|
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
|
|
|
import io.netty.handler.codec.http.FullHttpRequest;
|
|
|
|
import io.netty.handler.codec.http.HttpClientCodec;
|
2014-10-31 08:48:28 +01:00
|
|
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
2017-05-13 01:30:38 +02:00
|
|
|
import io.netty.handler.codec.http.HttpHeaders;
|
2014-08-25 10:50:37 +02:00
|
|
|
import io.netty.handler.codec.http.HttpMethod;
|
|
|
|
import io.netty.handler.codec.http.HttpResponse;
|
|
|
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
2018-03-23 11:58:43 +01:00
|
|
|
import io.netty.handler.codec.http.HttpUtil;
|
2014-08-25 10:50:37 +02:00
|
|
|
import io.netty.handler.codec.http.HttpVersion;
|
|
|
|
import io.netty.handler.codec.http.LastHttpContent;
|
2015-04-01 01:23:52 +02:00
|
|
|
import io.netty.util.AsciiString;
|
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.Arrays;
|
|
|
|
import java.util.Base64;
|
2014-08-25 10:50:37 +02:00
|
|
|
|
|
|
|
public final class HttpProxyHandler extends ProxyHandler {
|
|
|
|
|
|
|
|
private static final String PROTOCOL = "http";
|
|
|
|
private static final String AUTH_BASIC = "basic";
|
|
|
|
|
2019-02-12 17:04:09 +01:00
|
|
|
private static final byte[] BASIC_BYTES = "Basic ".getBytes(StandardCharsets.UTF_8);
|
|
|
|
|
2014-08-25 10:50:37 +02:00
|
|
|
private final HttpClientCodec codec = new HttpClientCodec();
|
|
|
|
private final String username;
|
|
|
|
private final String password;
|
|
|
|
private final CharSequence authorization;
|
2019-02-02 07:16:36 +01:00
|
|
|
private final HttpHeaders outboundHeaders;
|
2018-03-23 11:58:43 +01:00
|
|
|
private final boolean ignoreDefaultPortsInConnectHostHeader;
|
2014-08-25 10:50:37 +02:00
|
|
|
private HttpResponseStatus status;
|
2019-02-02 07:16:36 +01:00
|
|
|
private HttpHeaders inboundHeaders;
|
2014-08-25 10:50:37 +02:00
|
|
|
|
|
|
|
public HttpProxyHandler(SocketAddress proxyAddress) {
|
2017-05-13 01:30:38 +02:00
|
|
|
this(proxyAddress, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpProxyHandler(SocketAddress proxyAddress, HttpHeaders headers) {
|
2018-03-23 11:58:43 +01:00
|
|
|
this(proxyAddress, headers, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpProxyHandler(SocketAddress proxyAddress,
|
|
|
|
HttpHeaders headers,
|
|
|
|
boolean ignoreDefaultPortsInConnectHostHeader) {
|
2014-08-25 10:50:37 +02:00
|
|
|
super(proxyAddress);
|
|
|
|
username = null;
|
|
|
|
password = null;
|
|
|
|
authorization = null;
|
2019-02-02 07:16:36 +01:00
|
|
|
this.outboundHeaders = headers;
|
2018-03-23 11:58:43 +01:00
|
|
|
this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public HttpProxyHandler(SocketAddress proxyAddress, String username, String password) {
|
2017-05-13 01:30:38 +02:00
|
|
|
this(proxyAddress, username, password, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpProxyHandler(SocketAddress proxyAddress, String username, String password,
|
|
|
|
HttpHeaders headers) {
|
2018-03-23 11:58:43 +01:00
|
|
|
this(proxyAddress, username, password, headers, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpProxyHandler(SocketAddress proxyAddress,
|
|
|
|
String username,
|
|
|
|
String password,
|
|
|
|
HttpHeaders headers,
|
|
|
|
boolean ignoreDefaultPortsInConnectHostHeader) {
|
2014-08-25 10:50:37 +02:00
|
|
|
super(proxyAddress);
|
2019-02-04 10:32:25 +01:00
|
|
|
requireNonNull(username, "username");
|
|
|
|
requireNonNull(password, "password");
|
2014-08-25 10:50:37 +02:00
|
|
|
this.username = username;
|
|
|
|
this.password = password;
|
|
|
|
|
2019-02-12 17:04:09 +01:00
|
|
|
byte[] authzBase64 = Base64.getEncoder().encode(
|
|
|
|
(username + ':' + password).getBytes(StandardCharsets.UTF_8));
|
|
|
|
byte[] authzHeader = Arrays.copyOf(BASIC_BYTES, 6 + authzBase64.length);
|
|
|
|
System.arraycopy(authzBase64, 0, authzHeader, 6, authzBase64.length);
|
2014-08-25 10:50:37 +02:00
|
|
|
|
2019-02-12 17:04:09 +01:00
|
|
|
authorization = new AsciiString(authzHeader, /*copy=*/ false);
|
2017-05-13 01:30:38 +02:00
|
|
|
|
2019-02-02 07:16:36 +01:00
|
|
|
this.outboundHeaders = headers;
|
2018-03-23 11:58:43 +01:00
|
|
|
this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@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, codec);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void removeEncoder(ChannelHandlerContext ctx) throws Exception {
|
2016-01-10 22:19:55 +01:00
|
|
|
codec.removeOutboundHandler();
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void removeDecoder(ChannelHandlerContext ctx) throws Exception {
|
2016-01-10 22:19:55 +01:00
|
|
|
codec.removeInboundHandler();
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
InetSocketAddress raddr = destinationAddress();
|
2018-03-23 11:58:43 +01:00
|
|
|
|
|
|
|
String hostString = HttpUtil.formatHostnameForHttp(raddr);
|
|
|
|
int port = raddr.getPort();
|
|
|
|
String url = hostString + ":" + port;
|
|
|
|
String hostHeader = (ignoreDefaultPortsInConnectHostHeader && (port == 80 || port == 443)) ?
|
|
|
|
hostString :
|
|
|
|
url;
|
|
|
|
|
2014-08-25 10:50:37 +02:00
|
|
|
FullHttpRequest req = new DefaultFullHttpRequest(
|
2016-06-22 13:58:14 +02:00
|
|
|
HttpVersion.HTTP_1_1, HttpMethod.CONNECT,
|
2018-03-23 11:58:43 +01:00
|
|
|
url,
|
2014-08-25 10:50:37 +02:00
|
|
|
Unpooled.EMPTY_BUFFER, false);
|
|
|
|
|
2018-03-23 11:58:43 +01:00
|
|
|
req.headers().set(HttpHeaderNames.HOST, hostHeader);
|
2014-08-25 10:50:37 +02:00
|
|
|
|
|
|
|
if (authorization != null) {
|
2014-11-20 12:39:18 +01:00
|
|
|
req.headers().set(HttpHeaderNames.PROXY_AUTHORIZATION, authorization);
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
|
2019-02-02 07:16:36 +01:00
|
|
|
if (outboundHeaders != null) {
|
|
|
|
req.headers().add(outboundHeaders);
|
2017-05-13 01:30:38 +02:00
|
|
|
}
|
|
|
|
|
2014-08-25 10:50:37 +02:00
|
|
|
return req;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2019-03-01 19:48:29 +01:00
|
|
|
protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception {
|
2014-08-25 10:50:37 +02:00
|
|
|
if (response instanceof HttpResponse) {
|
|
|
|
if (status != null) {
|
2019-02-02 07:16:36 +01:00
|
|
|
throw new HttpProxyConnectException(exceptionMessage("too many responses"), /*headers=*/ null);
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
2019-02-02 07:16:36 +01:00
|
|
|
HttpResponse res = (HttpResponse) response;
|
|
|
|
status = res.status();
|
|
|
|
inboundHeaders = res.headers();
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
boolean finished = response instanceof LastHttpContent;
|
|
|
|
if (finished) {
|
|
|
|
if (status == null) {
|
2019-02-02 07:16:36 +01:00
|
|
|
throw new HttpProxyConnectException(exceptionMessage("missing response"), inboundHeaders);
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
if (status.code() != 200) {
|
2019-02-02 07:16:36 +01:00
|
|
|
throw new HttpProxyConnectException(exceptionMessage("status: " + status), inboundHeaders);
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return finished;
|
|
|
|
}
|
2019-02-02 07:16:36 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
}
|
2014-08-25 10:50:37 +02:00
|
|
|
}
|