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 HttpResponseStatus status;
74      private HttpHeaders inboundHeaders;
75  
76      public HttpProxyHandler(SocketAddress proxyAddress) {
77          this(proxyAddress, null);
78      }
79  
80      public HttpProxyHandler(SocketAddress proxyAddress, HttpHeaders headers) {
81          this(proxyAddress, headers, false);
82      }
83  
84      public HttpProxyHandler(SocketAddress proxyAddress,
85                              HttpHeaders headers,
86                              boolean ignoreDefaultPortsInConnectHostHeader) {
87          super(proxyAddress);
88          username = null;
89          password = null;
90          authorization = null;
91          this.outboundHeaders = headers;
92          this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
93      }
94  
95      public HttpProxyHandler(SocketAddress proxyAddress, String username, String password) {
96          this(proxyAddress, username, password, null);
97      }
98  
99      public HttpProxyHandler(SocketAddress proxyAddress, String username, String password,
100                             HttpHeaders headers) {
101         this(proxyAddress, username, password, headers, false);
102     }
103 
104     public HttpProxyHandler(SocketAddress proxyAddress,
105                             String username,
106                             String password,
107                             HttpHeaders headers,
108                             boolean ignoreDefaultPortsInConnectHostHeader) {
109         super(proxyAddress);
110         this.username = ObjectUtil.checkNotNull(username, "username");
111         this.password = ObjectUtil.checkNotNull(password, "password");
112 
113         ByteBuf authz = Unpooled.copiedBuffer(username + ':' + password, CharsetUtil.UTF_8);
114         ByteBuf authzBase64;
115         try {
116             authzBase64 = Base64.encode(authz, false);
117         } finally {
118             authz.release();
119         }
120         try {
121             authorization = new AsciiString("Basic " + authzBase64.toString(CharsetUtil.US_ASCII));
122         } finally {
123             authzBase64.release();
124         }
125 
126         this.outboundHeaders = headers;
127         this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
128     }
129 
130     @Override
131     public String protocol() {
132         return PROTOCOL;
133     }
134 
135     @Override
136     public String authScheme() {
137         return authorization != null? AUTH_BASIC : AUTH_NONE;
138     }
139 
140     public String username() {
141         return username;
142     }
143 
144     public String password() {
145         return password;
146     }
147 
148     @Override
149     protected void addCodec(ChannelHandlerContext ctx) throws Exception {
150         ChannelPipeline p = ctx.pipeline();
151         String name = ctx.name();
152         p.addBefore(name, null, codecWrapper);
153     }
154 
155     @Override
156     protected void removeEncoder(ChannelHandlerContext ctx) throws Exception {
157         codecWrapper.codec.removeOutboundHandler();
158     }
159 
160     @Override
161     protected void removeDecoder(ChannelHandlerContext ctx) throws Exception {
162         codecWrapper.codec.removeInboundHandler();
163     }
164 
165     @Override
166     protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception {
167         InetSocketAddress raddr = destinationAddress();
168 
169         String hostString = HttpUtil.formatHostnameForHttp(raddr);
170         int port = raddr.getPort();
171         String url = hostString + ":" + port;
172         String hostHeader = (ignoreDefaultPortsInConnectHostHeader && (port == 80 || port == 443)) ?
173                 hostString :
174                 url;
175 
176         HttpHeadersFactory headersFactory = DefaultHttpHeadersFactory.headersFactory().withValidation(false);
177         FullHttpRequest req = new DefaultFullHttpRequest(
178                 HttpVersion.HTTP_1_1, HttpMethod.CONNECT,
179                 url,
180                 Unpooled.EMPTY_BUFFER, headersFactory, headersFactory);
181 
182         req.headers().set(HttpHeaderNames.HOST, hostHeader);
183 
184         if (authorization != null) {
185             req.headers().set(HttpHeaderNames.PROXY_AUTHORIZATION, authorization);
186         }
187 
188         if (outboundHeaders != null) {
189             req.headers().add(outboundHeaders);
190         }
191 
192         return req;
193     }
194 
195     @Override
196     protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception {
197         if (response instanceof HttpResponse) {
198             if (status != null) {
199                 throw new HttpProxyConnectException(exceptionMessage("too many responses"), /*headers=*/ null);
200             }
201             HttpResponse res = (HttpResponse) response;
202             status = res.status();
203             inboundHeaders = res.headers();
204         }
205 
206         boolean finished = response instanceof LastHttpContent;
207         if (finished) {
208             if (status == null) {
209                 throw new HttpProxyConnectException(exceptionMessage("missing response"), inboundHeaders);
210             }
211             if (status.code() != 200) {
212                 throw new HttpProxyConnectException(exceptionMessage("status: " + status), inboundHeaders);
213             }
214         }
215 
216         return finished;
217     }
218 
219     /**
220      * Specific case of a connection failure, which may include headers from the proxy.
221      */
222     public static final class HttpProxyConnectException extends ProxyConnectException {
223         private static final long serialVersionUID = -8824334609292146066L;
224 
225         private final HttpHeaders headers;
226 
227         /**
228          * @param message The failure message.
229          * @param headers Header associated with the connection failure.  May be {@code null}.
230          */
231         public HttpProxyConnectException(String message, HttpHeaders headers) {
232             super(message);
233             this.headers = headers;
234         }
235 
236         /**
237          * Returns headers, if any.  May be {@code null}.
238          */
239         public HttpHeaders headers() {
240             return headers;
241         }
242     }
243 
244     private static final class HttpClientCodecWrapper implements ChannelInboundHandler, ChannelOutboundHandler {
245         final HttpClientCodec codec = new HttpClientCodec();
246 
247         @Override
248         public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
249             codec.handlerAdded(ctx);
250         }
251 
252         @Override
253         public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
254             codec.handlerRemoved(ctx);
255         }
256 
257         @Override
258         public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
259             codec.exceptionCaught(ctx, cause);
260         }
261 
262         @Override
263         public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
264             codec.channelRegistered(ctx);
265         }
266 
267         @Override
268         public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
269             codec.channelUnregistered(ctx);
270         }
271 
272         @Override
273         public void channelActive(ChannelHandlerContext ctx) throws Exception {
274             codec.channelActive(ctx);
275         }
276 
277         @Override
278         public void channelInactive(ChannelHandlerContext ctx) throws Exception {
279             codec.channelInactive(ctx);
280         }
281 
282         @Override
283         public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
284             codec.channelRead(ctx, msg);
285         }
286 
287         @Override
288         public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
289             codec.channelReadComplete(ctx);
290         }
291 
292         @Override
293         public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
294             codec.userEventTriggered(ctx, evt);
295         }
296 
297         @Override
298         public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
299             codec.channelWritabilityChanged(ctx);
300         }
301 
302         @Override
303         public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
304                          ChannelPromise promise) throws Exception {
305             codec.bind(ctx, localAddress, promise);
306         }
307 
308         @Override
309         public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
310                             ChannelPromise promise) throws Exception {
311             codec.connect(ctx, remoteAddress, localAddress, promise);
312         }
313 
314         @Override
315         public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
316             codec.disconnect(ctx, promise);
317         }
318 
319         @Override
320         public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
321             codec.close(ctx, promise);
322         }
323 
324         @Override
325         public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
326             codec.deregister(ctx, promise);
327         }
328 
329         @Override
330         public void read(ChannelHandlerContext ctx) throws Exception {
331             codec.read(ctx);
332         }
333 
334         @Override
335         public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
336             codec.write(ctx, msg, promise);
337         }
338 
339         @Override
340         public void flush(ChannelHandlerContext ctx) throws Exception {
341             codec.flush(ctx);
342         }
343     }
344 }