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