View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.proxy;
18  
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.Unpooled;
21  import io.netty.channel.ChannelHandlerContext;
22  import io.netty.channel.ChannelInboundHandler;
23  import io.netty.channel.ChannelOutboundHandler;
24  import io.netty.channel.ChannelPipeline;
25  import io.netty.channel.ChannelPromise;
26  import io.netty.handler.codec.base64.Base64;
27  import io.netty.handler.codec.http.DefaultFullHttpRequest;
28  import io.netty.handler.codec.http.FullHttpRequest;
29  import io.netty.handler.codec.http.HttpClientCodec;
30  import io.netty.handler.codec.http.HttpHeaderNames;
31  import io.netty.handler.codec.http.HttpHeaders;
32  import io.netty.handler.codec.http.DefaultHttpHeadersFactory;
33  import io.netty.handler.codec.http.HttpHeadersFactory;
34  import io.netty.handler.codec.http.HttpMethod;
35  import io.netty.handler.codec.http.HttpResponse;
36  import io.netty.handler.codec.http.HttpResponseStatus;
37  import io.netty.handler.codec.http.HttpUtil;
38  import io.netty.handler.codec.http.HttpVersion;
39  import io.netty.handler.codec.http.LastHttpContent;
40  import io.netty.util.AsciiString;
41  import io.netty.util.CharsetUtil;
42  import io.netty.util.internal.ObjectUtil;
43  
44  import java.net.InetSocketAddress;
45  import java.net.SocketAddress;
46  
47  /**
48   * Handler that establishes a blind forwarding proxy tunnel using
49   * <a href="https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.6">HTTP/1.1 CONNECT</a> request. It can be used to
50   * establish plaintext or secure tunnels.
51   * <p>
52   * HTTP users who need to connect to a
53   * <a href="https://datatracker.ietf.org/doc/html/rfc7230#page-10">message-forwarding HTTP proxy agent</a> instead of a
54   * tunneling proxy should not use this handler.
55   */
56  public final class HttpProxyHandler extends ProxyHandler {
57  
58      private static final String PROTOCOL = "http";
59      private static final String AUTH_BASIC = "basic";
60  
61      // Wrapper for the HttpClientCodec to prevent it to be removed by other handlers by mistake (for example the
62      // WebSocket*Handshaker.
63      //
64      // See:
65      // - https://github.com/netty/netty/issues/5201
66      // - https://github.com/netty/netty/issues/5070
67      private final HttpClientCodecWrapper codecWrapper = new HttpClientCodecWrapper();
68      private final String username;
69      private final String password;
70      private final CharSequence authorization;
71      private final HttpHeaders outboundHeaders;
72      private final boolean ignoreDefaultPortsInConnectHostHeader;
73      private final boolean validateInitialHeaders;
74      private HttpResponseStatus status;
75      private HttpHeaders inboundHeaders;
76  
77      public HttpProxyHandler(SocketAddress proxyAddress) {
78          this(proxyAddress, null);
79      }
80  
81      public HttpProxyHandler(SocketAddress proxyAddress, HttpHeaders headers) {
82          this(proxyAddress, headers, false);
83      }
84  
85      public HttpProxyHandler(SocketAddress proxyAddress,
86                              HttpHeaders headers,
87                              boolean ignoreDefaultPortsInConnectHostHeader) {
88          this(proxyAddress, headers, ignoreDefaultPortsInConnectHostHeader, true);
89      }
90  
91      public HttpProxyHandler(SocketAddress proxyAddress,
92                              HttpHeaders headers,
93                              boolean ignoreDefaultPortsInConnectHostHeader,
94                              boolean validateInitialHeaders) {
95          super(proxyAddress);
96          username = null;
97          password = null;
98          authorization = null;
99          this.outboundHeaders = headers;
100         this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
101         this.validateInitialHeaders = validateInitialHeaders;
102     }
103 
104     public HttpProxyHandler(SocketAddress proxyAddress, String username, String password) {
105         this(proxyAddress, username, password, null);
106     }
107 
108     public HttpProxyHandler(SocketAddress proxyAddress, String username, String password,
109                             HttpHeaders headers) {
110         this(proxyAddress, username, password, headers, false, true);
111     }
112 
113     public HttpProxyHandler(SocketAddress proxyAddress,
114                             String username,
115                             String password,
116                             HttpHeaders headers,
117                             boolean ignoreDefaultPortsInConnectHostHeader) {
118         this(proxyAddress, username, password, headers, ignoreDefaultPortsInConnectHostHeader, true);
119     }
120 
121     public HttpProxyHandler(SocketAddress proxyAddress,
122                             String username,
123                             String password,
124                             HttpHeaders headers,
125                             boolean ignoreDefaultPortsInConnectHostHeader,
126                             boolean validateInitialHeaders) {
127         super(proxyAddress);
128         this.username = ObjectUtil.checkNotNull(username, "username");
129         this.password = ObjectUtil.checkNotNull(password, "password");
130 
131         ByteBuf authz = Unpooled.copiedBuffer(username + ':' + password, CharsetUtil.UTF_8);
132         ByteBuf authzBase64;
133         try {
134             authzBase64 = Base64.encode(authz, false);
135         } finally {
136             authz.release();
137         }
138         try {
139             authorization = new AsciiString("Basic " + authzBase64.toString(CharsetUtil.US_ASCII));
140         } finally {
141             authzBase64.release();
142         }
143 
144         this.outboundHeaders = headers;
145         this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
146         this.validateInitialHeaders = validateInitialHeaders;
147     }
148 
149     @Override
150     public String protocol() {
151         return PROTOCOL;
152     }
153 
154     @Override
155     public String authScheme() {
156         return authorization != null? AUTH_BASIC : AUTH_NONE;
157     }
158 
159     public String username() {
160         return username;
161     }
162 
163     public String password() {
164         return password;
165     }
166 
167     @Override
168     protected void addCodec(ChannelHandlerContext ctx) throws Exception {
169         ChannelPipeline p = ctx.pipeline();
170         String name = ctx.name();
171         p.addBefore(name, null, codecWrapper);
172     }
173 
174     @Override
175     protected void removeEncoder(ChannelHandlerContext ctx) throws Exception {
176         codecWrapper.codec.removeOutboundHandler();
177     }
178 
179     @Override
180     protected void removeDecoder(ChannelHandlerContext ctx) throws Exception {
181         codecWrapper.codec.removeInboundHandler();
182     }
183 
184     @Override
185     protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception {
186         InetSocketAddress raddr = destinationAddress();
187 
188         String hostString = HttpUtil.formatHostnameForHttp(raddr);
189         int port = raddr.getPort();
190         String url = hostString + ":" + port;
191         String hostHeader = (ignoreDefaultPortsInConnectHostHeader && (port == 80 || port == 443)) ?
192                 hostString :
193                 url;
194 
195         HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory()
196                 .withValidation(validateInitialHeaders);
197         FullHttpRequest req = new DefaultFullHttpRequest(
198                 HttpVersion.HTTP_1_1, HttpMethod.CONNECT,
199                 url,
200                 Unpooled.EMPTY_BUFFER, headersFactory, headersFactory);
201 
202         req.headers().set(HttpHeaderNames.HOST, hostHeader);
203 
204         if (authorization != null) {
205             req.headers().set(HttpHeaderNames.PROXY_AUTHORIZATION, authorization);
206         }
207 
208         if (outboundHeaders != null) {
209             req.headers().add(outboundHeaders);
210         }
211 
212         return req;
213     }
214 
215     @Override
216     protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception {
217         if (response instanceof HttpResponse) {
218             if (status != null) {
219                 throw new HttpProxyConnectException(exceptionMessage("too many responses"), /*headers=*/ null);
220             }
221             HttpResponse res = (HttpResponse) response;
222             status = res.status();
223             inboundHeaders = res.headers();
224         }
225 
226         boolean finished = response instanceof LastHttpContent;
227         if (finished) {
228             if (status == null) {
229                 throw new HttpProxyConnectException(exceptionMessage("missing response"), inboundHeaders);
230             }
231             if (status.code() != 200) {
232                 throw new HttpProxyConnectException(exceptionMessage("status: " + status), inboundHeaders);
233             }
234         }
235 
236         return finished;
237     }
238 
239     /**
240      * Specific case of a connection failure, which may include headers from the proxy.
241      */
242     public static final class HttpProxyConnectException extends ProxyConnectException {
243         private static final long serialVersionUID = -8824334609292146066L;
244 
245         private final HttpHeaders headers;
246 
247         /**
248          * @param message The failure message.
249          * @param headers Header associated with the connection failure.  May be {@code null}.
250          */
251         public HttpProxyConnectException(String message, HttpHeaders headers) {
252             super(message);
253             this.headers = headers;
254         }
255 
256         /**
257          * Returns headers, if any.  May be {@code null}.
258          */
259         public HttpHeaders headers() {
260             return headers;
261         }
262     }
263 
264     private static final class HttpClientCodecWrapper implements ChannelInboundHandler, ChannelOutboundHandler {
265         final HttpClientCodec codec = new HttpClientCodec();
266 
267         @Override
268         public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
269             codec.handlerAdded(ctx);
270         }
271 
272         @Override
273         public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
274             codec.handlerRemoved(ctx);
275         }
276 
277         @Override
278         public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
279             codec.exceptionCaught(ctx, cause);
280         }
281 
282         @Override
283         public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
284             codec.channelRegistered(ctx);
285         }
286 
287         @Override
288         public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
289             codec.channelUnregistered(ctx);
290         }
291 
292         @Override
293         public void channelActive(ChannelHandlerContext ctx) throws Exception {
294             codec.channelActive(ctx);
295         }
296 
297         @Override
298         public void channelInactive(ChannelHandlerContext ctx) throws Exception {
299             codec.channelInactive(ctx);
300         }
301 
302         @Override
303         public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
304             codec.channelRead(ctx, msg);
305         }
306 
307         @Override
308         public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
309             codec.channelReadComplete(ctx);
310         }
311 
312         @Override
313         public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
314             codec.userEventTriggered(ctx, evt);
315         }
316 
317         @Override
318         public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
319             codec.channelWritabilityChanged(ctx);
320         }
321 
322         @Override
323         public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
324                          ChannelPromise promise) throws Exception {
325             codec.bind(ctx, localAddress, promise);
326         }
327 
328         @Override
329         public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
330                             ChannelPromise promise) throws Exception {
331             codec.connect(ctx, remoteAddress, localAddress, promise);
332         }
333 
334         @Override
335         public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
336             codec.disconnect(ctx, promise);
337         }
338 
339         @Override
340         public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
341             codec.close(ctx, promise);
342         }
343 
344         @Override
345         public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
346             codec.deregister(ctx, promise);
347         }
348 
349         @Override
350         public void read(ChannelHandlerContext ctx) throws Exception {
351             codec.read(ctx);
352         }
353 
354         @Override
355         public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
356             codec.write(ctx, msg, promise);
357         }
358 
359         @Override
360         public void flush(ChannelHandlerContext ctx) throws Exception {
361             codec.flush(ctx);
362         }
363     }
364 }