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.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
39
40 public abstract class ProxyHandler extends ChannelDuplexHandler {
41
42 private static final InternalLogger logger = InternalLoggerFactory.getInstance(ProxyHandler.class);
43
44
45
46
47 private static final long DEFAULT_CONNECT_TIMEOUT_MILLIS = 10000;
48
49
50
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
77
78 public abstract String protocol();
79
80
81
82
83 public abstract String authScheme();
84
85
86
87
88 @SuppressWarnings("unchecked")
89 public final <T extends SocketAddress> T proxyAddress() {
90 return (T) proxyAddress;
91 }
92
93
94
95
96 @SuppressWarnings("unchecked")
97 public final <T extends SocketAddress> T destinationAddress() {
98 return (T) destinationAddress;
99 }
100
101
102
103
104 public final boolean isConnected() {
105 return connectPromise.isSuccess();
106 }
107
108
109
110
111
112 public final Future<Channel> connectFuture() {
113 return connectPromise;
114 }
115
116
117
118
119
120 public final long connectTimeoutMillis() {
121 return connectTimeoutMillis;
122 }
123
124
125
126
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
143
144 sendInitialMessage(ctx);
145 } else {
146
147
148 }
149 }
150
151 @Override
152 public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
153
154
155
156
157
158
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
170
171 protected abstract void addCodec(ChannelHandlerContext ctx) throws Exception;
172
173
174
175
176 protected abstract void removeEncoder(ChannelHandlerContext ctx) throws Exception;
177
178
179
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
205
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
230
231
232
233 protected abstract Object newInitialMessage(ChannelHandlerContext ctx) throws Exception;
234
235
236
237
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
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
259 setConnectFailure(cause);
260 }
261 }
262
263 @Override
264 public final void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
265 if (finished) {
266
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
290
291
292
293
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
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
382
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 }