1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
40
41 public abstract class ProxyHandler extends ChannelDuplexHandler {
42
43 private static final InternalLogger logger = InternalLoggerFactory.getInstance(ProxyHandler.class);
44
45
46
47
48 private static final long DEFAULT_CONNECT_TIMEOUT_MILLIS = 10000;
49
50
51
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
81
82 public abstract String protocol();
83
84
85
86
87 public abstract String authScheme();
88
89
90
91
92 @SuppressWarnings("unchecked")
93 public final <T extends SocketAddress> T proxyAddress() {
94 return (T) proxyAddress;
95 }
96
97
98
99
100 @SuppressWarnings("unchecked")
101 public final <T extends SocketAddress> T destinationAddress() {
102 return (T) destinationAddress;
103 }
104
105
106
107
108 public final boolean isConnected() {
109 return connectPromise.isSuccess();
110 }
111
112
113
114
115
116 public final Future<Channel> connectFuture() {
117 return connectPromise;
118 }
119
120
121
122
123
124 public final long connectTimeoutMillis() {
125 return connectTimeoutMillis;
126 }
127
128
129
130
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
147
148 sendInitialMessage(ctx);
149 } else {
150
151
152 }
153 }
154
155 @Override
156 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
157
158
159
160
161
162
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
174
175 protected abstract void addCodec(ChannelHandlerContext ctx) throws Exception;
176
177
178
179
180 protected abstract void removeEncoder(ChannelHandlerContext ctx) throws Exception;
181
182
183
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
209
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
234
235
236
237 protected abstract Object newInitialMessage(ChannelHandlerContext ctx) throws Exception;
238
239
240
241
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
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
263 setConnectFailure(cause);
264 }
265 }
266
267 @Override
268 public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
269 if (finished) {
270
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
294
295
296
297
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
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
386
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 }