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