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.channel.Channel;
20  import io.netty.channel.ChannelDuplexHandler;
21  import io.netty.channel.ChannelFuture;
22  import io.netty.channel.ChannelFutureListener;
23  import io.netty.channel.ChannelHandlerContext;
24  import io.netty.channel.ChannelPromise;
25  import io.netty.channel.PendingWriteQueue;
26  import io.netty.util.ReferenceCountUtil;
27  import io.netty.util.concurrent.DefaultPromise;
28  import io.netty.util.concurrent.EventExecutor;
29  import io.netty.util.concurrent.Future;
30  import io.netty.util.internal.ObjectUtil;
31  import io.netty.util.internal.logging.InternalLogger;
32  import io.netty.util.internal.logging.InternalLoggerFactory;
33  
34  import java.net.SocketAddress;
35  import java.nio.channels.ConnectionPendingException;
36  import java.util.concurrent.TimeUnit;
37  
38  /**
39   * A common abstraction for protocols that establish blind forwarding proxy tunnels.
40   */
41  public abstract class ProxyHandler extends ChannelDuplexHandler {
42  
43      private static final InternalLogger logger = InternalLoggerFactory.getInstance(ProxyHandler.class);
44  
45      /**
46       * The default connect timeout: 10 seconds.
47       */
48      private static final long DEFAULT_CONNECT_TIMEOUT_MILLIS = 10000;
49  
50      /**
51       * A string that signifies 'no authentication' or 'anonymous'.
52       */
53      static final String AUTH_NONE = "none";
54  
55      private final SocketAddress proxyAddress;
56      private volatile SocketAddress destinationAddress;
57      private volatile long connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS;
58  
59      private volatile ChannelHandlerContext ctx;
60      private PendingWriteQueue pendingWrites;
61      private boolean finished;
62      private boolean suppressChannelReadComplete;
63      private boolean flushedPrematurely;
64      private final LazyChannelPromise connectPromise = new LazyChannelPromise();
65      private Future<?> connectTimeoutFuture;
66      private final ChannelFutureListener writeListener = new ChannelFutureListener() {
67          @Override
68          public void operationComplete(ChannelFuture future) throws Exception {
69              if (!future.isSuccess()) {
70                  setConnectFailure(future.cause());
71              }
72          }
73      };
74  
75      protected ProxyHandler(SocketAddress proxyAddress) {
76          this.proxyAddress = ObjectUtil.checkNotNull(proxyAddress, "proxyAddress");
77      }
78  
79      /**
80       * Returns the name of the proxy protocol in use.
81       */
82      public abstract String protocol();
83  
84      /**
85       * Returns the name of the authentication scheme in use.
86       */
87      public abstract String authScheme();
88  
89      /**
90       * Returns the address of the proxy server.
91       */
92      @SuppressWarnings("unchecked")
93      public final <T extends SocketAddress> T proxyAddress() {
94          return (T) proxyAddress;
95      }
96  
97      /**
98       * Returns the address of the destination to connect to via the proxy server.
99       */
100     @SuppressWarnings("unchecked")
101     public final <T extends SocketAddress> T destinationAddress() {
102         return (T) destinationAddress;
103     }
104 
105     /**
106      * Returns {@code true} if and only if the connection to the destination has been established successfully.
107      */
108     public final boolean isConnected() {
109         return connectPromise.isSuccess();
110     }
111 
112     /**
113      * Returns a {@link Future} that is notified when the connection to the destination has been established
114      * or the connection attempt has failed.
115      */
116     public final Future<Channel> connectFuture() {
117         return connectPromise;
118     }
119 
120     /**
121      * Returns the connect timeout in millis.  If the connection attempt to the destination does not finish within
122      * the timeout, the connection attempt will be failed.
123      */
124     public final long connectTimeoutMillis() {
125         return connectTimeoutMillis;
126     }
127 
128     /**
129      * Sets the connect timeout in millis.  If the connection attempt to the destination does not finish within
130      * the timeout, the connection attempt will be failed.
131      */
132     public final void setConnectTimeoutMillis(long connectTimeoutMillis) {
133         if (connectTimeoutMillis <= 0) {
134             connectTimeoutMillis = 0;
135         }
136 
137         this.connectTimeoutMillis = connectTimeoutMillis;
138     }
139 
140     @Override
141     public final void handlerAdded(ChannelHandlerContext ctx) throws Exception {
142         this.ctx = ctx;
143         addCodec(ctx);
144 
145         if (ctx.channel().isActive()) {
146             // channelActive() event has been fired already, which means this.channelActive() will
147             // not be invoked. We have to initialize here instead.
148             sendInitialMessage(ctx);
149         } else {
150             // channelActive() event has not been fired yet.  this.channelOpen() will be invoked
151             // and initialization will occur there.
152         }
153     }
154 
155     @Override
156     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
157         // The handler can be removed before the connection to the destination was established, for example because
158         // the connection to the proxy server failed. The channel never became active in that case, so
159         // channelInactive(...) is not called. Fail what is still pending, as otherwise the queued writes and
160         // connectFuture() would never be completed and the queued messages would leak.
161         // If the handshake already finished (the handler may be removed from a ProxyConnectionEvent listener while
162         // setConnectSuccess() runs), setConnectSuccess() or setConnectFailure() completes everything.
163         if (!finished) {
164             Exception cause = new ProxyConnectException(
165                     exceptionMessage("handler removed before the connection was established"));
166             failPendingWrites(cause);
167             connectPromise.tryFailure(cause);
168             cancelConnectTimeoutFuture();
169         }
170     }
171 
172     /**
173      * Adds the codec handlers required to communicate with the proxy server.
174      */
175     protected abstract void addCodec(ChannelHandlerContext ctx) throws Exception;
176 
177     /**
178      * Removes the encoders added in {@link #addCodec(ChannelHandlerContext)}.
179      */
180     protected abstract void removeEncoder(ChannelHandlerContext ctx) throws Exception;
181 
182     /**
183      * Removes the decoders added in {@link #addCodec(ChannelHandlerContext)}.
184      */
185     protected abstract void removeDecoder(ChannelHandlerContext ctx) throws Exception;
186 
187     @Override
188     public final void connect(
189             ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
190             ChannelPromise promise) throws Exception {
191 
192         if (destinationAddress != null) {
193             promise.setFailure(new ConnectionPendingException());
194             return;
195         }
196 
197         destinationAddress = remoteAddress;
198         ctx.connect(proxyAddress, localAddress, promise);
199     }
200 
201     @Override
202     public final void channelActive(ChannelHandlerContext ctx) throws Exception {
203         sendInitialMessage(ctx);
204         ctx.fireChannelActive();
205     }
206 
207     /**
208      * Sends the initial message to be sent to the proxy server. This method also starts a timeout task which marks
209      * the {@link #connectPromise} as failure if the connection attempt does not success within the timeout.
210      */
211     private void sendInitialMessage(final ChannelHandlerContext ctx) throws Exception {
212         final long connectTimeoutMillis = this.connectTimeoutMillis;
213         if (connectTimeoutMillis > 0) {
214             connectTimeoutFuture = ctx.executor().schedule(new Runnable() {
215                 @Override
216                 public void run() {
217                     if (!connectPromise.isDone()) {
218                         setConnectFailure(new ProxyConnectException(exceptionMessage("timeout")));
219                     }
220                 }
221             }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
222         }
223 
224         final Object initialMessage = newInitialMessage(ctx);
225         if (initialMessage != null) {
226             sendToProxyServer(initialMessage);
227         }
228 
229         readIfNeeded(ctx);
230     }
231 
232     /**
233      * Returns a new message that is sent at first time when the connection to the proxy server has been established.
234      *
235      * @return the initial message, or {@code null} if the proxy server is expected to send the first message instead
236      */
237     protected abstract Object newInitialMessage(ChannelHandlerContext ctx) throws Exception;
238 
239     /**
240      * Sends the specified message to the proxy server.  Use this method to send a response to the proxy server in
241      * {@link #handleResponse(ChannelHandlerContext, Object)}.
242      */
243     protected final void sendToProxyServer(Object msg) {
244         ctx.writeAndFlush(msg).addListener(writeListener);
245     }
246 
247     @Override
248     public final void channelInactive(ChannelHandlerContext ctx) throws Exception {
249         if (finished) {
250             ctx.fireChannelInactive();
251         } else {
252             // Disconnected before connected to the destination.
253             setConnectFailure(new ProxyConnectException(exceptionMessage("disconnected")));
254         }
255     }
256 
257     @Override
258     public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
259         if (finished) {
260             ctx.fireExceptionCaught(cause);
261         } else {
262             // Exception was raised before the connection attempt is finished.
263             setConnectFailure(cause);
264         }
265     }
266 
267     @Override
268     public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
269         if (finished) {
270             // Received a message after the connection has been established; pass through.
271             suppressChannelReadComplete = false;
272             ctx.fireChannelRead(msg);
273         } else {
274             suppressChannelReadComplete = true;
275             Throwable cause = null;
276             try {
277                 boolean done = handleResponse(ctx, msg);
278                 if (done) {
279                     setConnectSuccess();
280                 }
281             } catch (Throwable t) {
282                 cause = t;
283             } finally {
284                 ReferenceCountUtil.release(msg);
285                 if (cause != null) {
286                     setConnectFailure(cause);
287                 }
288             }
289         }
290     }
291 
292     /**
293      * Handles the message received from the proxy server.
294      *
295      * @return {@code true} if the connection to the destination has been established,
296      *         {@code false} if the connection to the destination has not been established and more messages are
297      *         expected from the proxy server
298      */
299     protected abstract boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception;
300 
301     private void setConnectSuccess() {
302         finished = true;
303         cancelConnectTimeoutFuture();
304 
305         if (!connectPromise.isDone()) {
306             boolean removedCodec = true;
307 
308             removedCodec &= safeRemoveEncoder();
309 
310             ctx.fireUserEventTriggered(
311                     new ProxyConnectionEvent(protocol(), authScheme(), proxyAddress, destinationAddress));
312 
313             removedCodec &= safeRemoveDecoder();
314 
315             if (removedCodec) {
316                 writePendingWrites();
317 
318                 if (flushedPrematurely) {
319                     ctx.flush();
320                 }
321                 connectPromise.trySuccess(ctx.channel());
322             } else {
323                 // We are at inconsistent state because we failed to remove all codec handlers.
324                 Exception cause = new ProxyConnectException(
325                         "failed to remove all codec handlers added by the proxy handler; bug?");
326                 failPendingWritesAndClose(cause);
327             }
328         }
329     }
330 
331     private boolean safeRemoveDecoder() {
332         try {
333             removeDecoder(ctx);
334             return true;
335         } catch (Exception e) {
336             logger.warn("Failed to remove proxy decoders:", e);
337         }
338 
339         return false;
340     }
341 
342     private boolean safeRemoveEncoder() {
343         try {
344             removeEncoder(ctx);
345             return true;
346         } catch (Exception e) {
347             logger.warn("Failed to remove proxy encoders:", e);
348         }
349 
350         return false;
351     }
352 
353     private void setConnectFailure(Throwable cause) {
354         finished = true;
355         cancelConnectTimeoutFuture();
356 
357         if (!connectPromise.isDone()) {
358 
359             if (!(cause instanceof ProxyConnectException)) {
360                 cause = new ProxyConnectException(
361                         exceptionMessage(cause.toString()), cause);
362             }
363 
364             safeRemoveDecoder();
365             safeRemoveEncoder();
366             failPendingWritesAndClose(cause);
367         }
368     }
369 
370     private void failPendingWritesAndClose(Throwable cause) {
371         failPendingWrites(cause);
372         connectPromise.tryFailure(cause);
373         ctx.fireExceptionCaught(cause);
374         ctx.close();
375     }
376 
377     private void cancelConnectTimeoutFuture() {
378         if (connectTimeoutFuture != null) {
379             connectTimeoutFuture.cancel(false);
380             connectTimeoutFuture = null;
381         }
382     }
383 
384     /**
385      * Decorates the specified exception message with the common information such as the current protocol,
386      * authentication scheme, proxy address, and destination address.
387      */
388     protected final String exceptionMessage(String msg) {
389         if (msg == null) {
390             msg = "";
391         }
392 
393         StringBuilder buf = new StringBuilder(128 + msg.length())
394             .append(protocol())
395             .append(", ")
396             .append(authScheme())
397             .append(", ")
398             .append(proxyAddress)
399             .append(" => ")
400             .append(destinationAddress);
401         if (!msg.isEmpty()) {
402             buf.append(", ").append(msg);
403         }
404 
405         return buf.toString();
406     }
407 
408     @Override
409     public final void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
410         if (suppressChannelReadComplete) {
411             suppressChannelReadComplete = false;
412 
413             readIfNeeded(ctx);
414         } else {
415             ctx.fireChannelReadComplete();
416         }
417     }
418 
419     @Override
420     public final void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
421         if (finished) {
422             writePendingWrites();
423             ctx.write(msg, promise);
424         } else {
425             addPendingWrite(ctx, msg, promise);
426         }
427     }
428 
429     @Override
430     public final void flush(ChannelHandlerContext ctx) throws Exception {
431         if (finished) {
432             writePendingWrites();
433             ctx.flush();
434         } else {
435             flushedPrematurely = true;
436         }
437     }
438 
439     private static void readIfNeeded(ChannelHandlerContext ctx) {
440         if (!ctx.channel().config().isAutoRead()) {
441             ctx.read();
442         }
443     }
444 
445     private void writePendingWrites() {
446         if (pendingWrites != null) {
447             pendingWrites.removeAndWriteAll();
448             pendingWrites = null;
449         }
450     }
451 
452     private void failPendingWrites(Throwable cause) {
453         if (pendingWrites != null) {
454             pendingWrites.removeAndFailAll(cause);
455             pendingWrites = null;
456         }
457     }
458 
459     private void addPendingWrite(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
460         PendingWriteQueue pendingWrites = this.pendingWrites;
461         if (pendingWrites == null) {
462             this.pendingWrites = pendingWrites = new PendingWriteQueue(ctx);
463         }
464         pendingWrites.add(msg, promise);
465     }
466 
467     private final class LazyChannelPromise extends DefaultPromise<Channel> {
468         @Override
469         protected EventExecutor executor() {
470             if (ctx == null) {
471                 throw new IllegalStateException();
472             }
473             return ctx.executor();
474         }
475     }
476 }