View Javadoc

1   package org.eclipse.jetty.server.handler;
2   //========================================================================
3   //Copyright 2011-2012 Mort Bay Consulting Pty. Ltd.
4   //------------------------------------------------------------------------
5   //All rights reserved. This program and the accompanying materials
6   //are made available under the terms of the Eclipse Public License v1.0
7   //and Apache License v2.0 which accompanies this distribution.
8   //The Eclipse Public License is available at
9   //http://www.eclipse.org/legal/epl-v10.html
10  //The Apache License v2.0 is available at
11  //http://www.opensource.org/licenses/apache2.0.php
12  //You may elect to redistribute this code under either of these licenses.
13  //========================================================================
14  
15  import java.io.IOException;
16  import java.net.InetSocketAddress;
17  import java.net.SocketException;
18  import java.net.SocketTimeoutException;
19  import java.nio.channels.ClosedChannelException;
20  import java.nio.channels.SelectionKey;
21  import java.nio.channels.SocketChannel;
22  import java.util.Arrays;
23  import java.util.concurrent.ConcurrentHashMap;
24  import java.util.concurrent.ConcurrentMap;
25  import java.util.concurrent.CountDownLatch;
26  import java.util.concurrent.TimeUnit;
27  import javax.servlet.ServletException;
28  import javax.servlet.http.HttpServletRequest;
29  import javax.servlet.http.HttpServletResponse;
30  
31  import org.eclipse.jetty.http.HttpMethods;
32  import org.eclipse.jetty.http.HttpParser;
33  import org.eclipse.jetty.io.AsyncEndPoint;
34  import org.eclipse.jetty.io.Buffer;
35  import org.eclipse.jetty.io.ConnectedEndPoint;
36  import org.eclipse.jetty.io.Connection;
37  import org.eclipse.jetty.io.EndPoint;
38  import org.eclipse.jetty.io.nio.AsyncConnection;
39  import org.eclipse.jetty.io.nio.IndirectNIOBuffer;
40  import org.eclipse.jetty.io.nio.SelectChannelEndPoint;
41  import org.eclipse.jetty.io.nio.SelectorManager;
42  import org.eclipse.jetty.server.AbstractHttpConnection;
43  import org.eclipse.jetty.server.Handler;
44  import org.eclipse.jetty.server.Request;
45  import org.eclipse.jetty.server.Server;
46  import org.eclipse.jetty.util.HostMap;
47  import org.eclipse.jetty.util.TypeUtil;
48  import org.eclipse.jetty.util.component.LifeCycle;
49  import org.eclipse.jetty.util.log.Log;
50  import org.eclipse.jetty.util.log.Logger;
51  import org.eclipse.jetty.util.thread.ThreadPool;
52  
53  /**
54   * <p>Implementation of a tunneling proxy that supports HTTP CONNECT.</p>
55   * <p>To work as CONNECT proxy, objects of this class must be instantiated using the no-arguments
56   * constructor, since the remote server information will be present in the CONNECT URI.</p>
57   */
58  public class ConnectHandler extends HandlerWrapper
59  {
60      private static final Logger LOG = Log.getLogger(ConnectHandler.class);
61      private final SelectorManager _selectorManager = new Manager();
62      private volatile int _connectTimeout = 5000;
63      private volatile int _writeTimeout = 30000;
64      private volatile ThreadPool _threadPool;
65      private volatile boolean _privateThreadPool;
66      private HostMap<String> _white = new HostMap<String>();
67      private HostMap<String> _black = new HostMap<String>();
68  
69      public ConnectHandler()
70      {
71          this(null);
72      }
73  
74      public ConnectHandler(String[] white, String[] black)
75      {
76          this(null, white, black);
77      }
78  
79      public ConnectHandler(Handler handler)
80      {
81          setHandler(handler);
82      }
83  
84      public ConnectHandler(Handler handler, String[] white, String[] black)
85      {
86          setHandler(handler);
87          set(white, _white);
88          set(black, _black);
89      }
90  
91      /**
92       * @return the timeout, in milliseconds, to connect to the remote server
93       */
94      public int getConnectTimeout()
95      {
96          return _connectTimeout;
97      }
98  
99      /**
100      * @param connectTimeout the timeout, in milliseconds, to connect to the remote server
101      */
102     public void setConnectTimeout(int connectTimeout)
103     {
104         _connectTimeout = connectTimeout;
105     }
106 
107     /**
108      * @return the timeout, in milliseconds, to write data to a peer
109      */
110     public int getWriteTimeout()
111     {
112         return _writeTimeout;
113     }
114 
115     /**
116      * @param writeTimeout the timeout, in milliseconds, to write data to a peer
117      */
118     public void setWriteTimeout(int writeTimeout)
119     {
120         _writeTimeout = writeTimeout;
121     }
122 
123     @Override
124     public void setServer(Server server)
125     {
126         super.setServer(server);
127 
128         server.getContainer().update(this, null, _selectorManager, "selectManager");
129 
130         if (_privateThreadPool)
131             server.getContainer().update(this, null, _privateThreadPool, "threadpool", true);
132         else
133             _threadPool = server.getThreadPool();
134     }
135 
136     /**
137      * @return the thread pool
138      */
139     public ThreadPool getThreadPool()
140     {
141         return _threadPool;
142     }
143 
144     /**
145      * @param threadPool the thread pool
146      */
147     public void setThreadPool(ThreadPool threadPool)
148     {
149         if (getServer() != null)
150             getServer().getContainer().update(this, _privateThreadPool ? _threadPool : null, threadPool, "threadpool", true);
151         _privateThreadPool = threadPool != null;
152         _threadPool = threadPool;
153     }
154 
155     @Override
156     protected void doStart() throws Exception
157     {
158         super.doStart();
159 
160         if (_threadPool == null)
161         {
162             _threadPool = getServer().getThreadPool();
163             _privateThreadPool = false;
164         }
165         if (_threadPool instanceof LifeCycle && !((LifeCycle)_threadPool).isRunning())
166             ((LifeCycle)_threadPool).start();
167 
168         _selectorManager.start();
169     }
170 
171     @Override
172     protected void doStop() throws Exception
173     {
174         _selectorManager.stop();
175 
176         ThreadPool threadPool = _threadPool;
177         if (_privateThreadPool && _threadPool != null && threadPool instanceof LifeCycle)
178             ((LifeCycle)threadPool).stop();
179 
180         super.doStop();
181     }
182 
183     @Override
184     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
185     {
186         if (HttpMethods.CONNECT.equalsIgnoreCase(request.getMethod()))
187         {
188             LOG.debug("CONNECT request for {}", request.getRequestURI());
189             try
190             {
191                 handleConnect(baseRequest, request, response, request.getRequestURI());
192             }
193             catch(Exception e)
194             {
195                 LOG.warn("ConnectHandler "+baseRequest.getUri()+" "+ e);
196                 LOG.debug(e);
197             }
198         }
199         else
200         {
201             super.handle(target, baseRequest, request, response);
202         }
203     }
204 
205     /**
206      * <p>Handles a CONNECT request.</p>
207      * <p>CONNECT requests may have authentication headers such as <code>Proxy-Authorization</code>
208      * that authenticate the client with the proxy.</p>
209      *
210      * @param baseRequest   Jetty-specific http request
211      * @param request       the http request
212      * @param response      the http response
213      * @param serverAddress the remote server address in the form {@code host:port}
214      * @throws ServletException if an application error occurs
215      * @throws IOException      if an I/O error occurs
216      */
217     protected void handleConnect(Request baseRequest, HttpServletRequest request, HttpServletResponse response, String serverAddress) throws ServletException, IOException
218     {
219         boolean proceed = handleAuthentication(request, response, serverAddress);
220         if (!proceed)
221             return;
222 
223         String host = serverAddress;
224         int port = 80;
225         int colon = serverAddress.indexOf(':');
226         if (colon > 0)
227         {
228             host = serverAddress.substring(0, colon);
229             port = Integer.parseInt(serverAddress.substring(colon + 1));
230         }
231 
232         if (!validateDestination(host))
233         {
234             LOG.info("ProxyHandler: Forbidden destination " + host);
235             response.setStatus(HttpServletResponse.SC_FORBIDDEN);
236             baseRequest.setHandled(true);
237             return;
238         }
239 
240         SocketChannel channel;
241 
242         try
243         {
244             channel = connectToServer(request,host,port);
245         }
246         catch (SocketException se)
247         {
248             LOG.info("ConnectHandler: SocketException " + se.getMessage());
249             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
250             baseRequest.setHandled(true);
251             return;
252         }
253         catch (SocketTimeoutException ste)
254         {
255             LOG.info("ConnectHandler: SocketTimeoutException" + ste.getMessage());
256             response.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT);
257             baseRequest.setHandled(true);
258             return;
259         }
260         catch (IOException ioe)
261         {
262             LOG.info("ConnectHandler: IOException" + ioe.getMessage());
263             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
264             baseRequest.setHandled(true);
265             return;
266         }
267 
268         // Transfer unread data from old connection to new connection
269         // We need to copy the data to avoid races:
270         // 1. when this unread data is written and the server replies before the clientToProxy
271         // connection is installed (it is only installed after returning from this method)
272         // 2. when the client sends data before this unread data has been written.
273         AbstractHttpConnection httpConnection = AbstractHttpConnection.getCurrentConnection();
274         Buffer headerBuffer = ((HttpParser)httpConnection.getParser()).getHeaderBuffer();
275         Buffer bodyBuffer = ((HttpParser)httpConnection.getParser()).getBodyBuffer();
276         int length = headerBuffer == null ? 0 : headerBuffer.length();
277         length += bodyBuffer == null ? 0 : bodyBuffer.length();
278         IndirectNIOBuffer buffer = null;
279         if (length > 0)
280         {
281             buffer = new IndirectNIOBuffer(length);
282             if (headerBuffer != null)
283             {
284                 buffer.put(headerBuffer);
285                 headerBuffer.clear();
286             }
287             if (bodyBuffer != null)
288             {
289                 buffer.put(bodyBuffer);
290                 bodyBuffer.clear();
291             }
292         }
293 
294         ConcurrentMap<String, Object> context = new ConcurrentHashMap<String, Object>();
295         prepareContext(request, context);
296 
297         ClientToProxyConnection clientToProxy = prepareConnections(context, channel, buffer);
298 
299         // CONNECT expects a 200 response
300         response.setStatus(HttpServletResponse.SC_OK);
301 
302         // Prevent close
303         baseRequest.getConnection().getGenerator().setPersistent(true);
304 
305         // Close to force last flush it so that the client receives it
306         response.getOutputStream().close();
307 
308         upgradeConnection(request, response, clientToProxy);
309     }
310 
311     private ClientToProxyConnection prepareConnections(ConcurrentMap<String, Object> context, SocketChannel channel, Buffer buffer)
312     {
313         AbstractHttpConnection httpConnection = AbstractHttpConnection.getCurrentConnection();
314         ProxyToServerConnection proxyToServer = newProxyToServerConnection(context, buffer);
315         ClientToProxyConnection clientToProxy = newClientToProxyConnection(context, channel, httpConnection.getEndPoint(), httpConnection.getTimeStamp());
316         clientToProxy.setConnection(proxyToServer);
317         proxyToServer.setConnection(clientToProxy);
318         return clientToProxy;
319     }
320 
321     /**
322      * <p>Handles the authentication before setting up the tunnel to the remote server.</p>
323      * <p>The default implementation returns true.</p>
324      *
325      * @param request  the HTTP request
326      * @param response the HTTP response
327      * @param address  the address of the remote server in the form {@code host:port}.
328      * @return true to allow to connect to the remote host, false otherwise
329      * @throws ServletException to report a server error to the caller
330      * @throws IOException      to report a server error to the caller
331      */
332     protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) throws ServletException, IOException
333     {
334         return true;
335     }
336 
337     protected ClientToProxyConnection newClientToProxyConnection(ConcurrentMap<String, Object> context, SocketChannel channel, EndPoint endPoint, long timeStamp)
338     {
339         return new ClientToProxyConnection(context, channel, endPoint, timeStamp);
340     }
341 
342     protected ProxyToServerConnection newProxyToServerConnection(ConcurrentMap<String, Object> context, Buffer buffer)
343     {
344         return new ProxyToServerConnection(context, buffer);
345     }
346 
347     // may return null
348     private SocketChannel connectToServer(HttpServletRequest request, String host, int port) throws IOException
349     {
350         SocketChannel channel = connect(request, host, port);      
351         channel.configureBlocking(false);
352         return channel;
353     }
354 
355     /**
356      * <p>Establishes a connection to the remote server.</p>
357      *
358      * @param request the HTTP request that initiated the tunnel
359      * @param host    the host to connect to
360      * @param port    the port to connect to
361      * @return a {@link SocketChannel} connected to the remote server
362      * @throws IOException if the connection cannot be established
363      */
364     protected SocketChannel connect(HttpServletRequest request, String host, int port) throws IOException
365     {
366         SocketChannel channel = SocketChannel.open();
367 
368         if (channel == null)
369         {
370             throw new IOException("unable to connect to " + host + ":" + port);
371         }
372 
373         try
374         {
375             // Connect to remote server
376             LOG.debug("Establishing connection to {}:{}", host, port);
377             channel.socket().setTcpNoDelay(true);
378             channel.socket().connect(new InetSocketAddress(host, port), getConnectTimeout());
379             LOG.debug("Established connection to {}:{}", host, port);
380             return channel;
381         }
382         catch (IOException x)
383         {
384             LOG.debug("Failed to establish connection to " + host + ":" + port, x);
385             try
386             {
387                 channel.close();
388             }
389             catch (IOException xx)
390             {
391                 LOG.ignore(xx);
392             }
393             throw x;
394         }
395     }
396 
397     protected void prepareContext(HttpServletRequest request, ConcurrentMap<String, Object> context)
398     {
399     }
400 
401     private void upgradeConnection(HttpServletRequest request, HttpServletResponse response, Connection connection) throws IOException
402     {
403         // Set the new connection as request attribute and change the status to 101
404         // so that Jetty understands that it has to upgrade the connection
405         request.setAttribute("org.eclipse.jetty.io.Connection", connection);
406         response.setStatus(HttpServletResponse.SC_SWITCHING_PROTOCOLS);
407         LOG.debug("Upgraded connection to {}", connection);
408     }
409 
410     private void register(SocketChannel channel, ProxyToServerConnection proxyToServer) throws IOException
411     {
412         _selectorManager.register(channel, proxyToServer);
413         proxyToServer.waitReady(_connectTimeout);
414     }
415 
416     /**
417      * <p>Reads (with non-blocking semantic) into the given {@code buffer} from the given {@code endPoint}.</p>
418      *
419      * @param endPoint the endPoint to read from
420      * @param buffer   the buffer to read data into
421      * @param context  the context information related to the connection
422      * @return the number of bytes read (possibly 0 since the read is non-blocking)
423      *         or -1 if the channel has been closed remotely
424      * @throws IOException if the endPoint cannot be read
425      */
426     protected int read(EndPoint endPoint, Buffer buffer, ConcurrentMap<String, Object> context) throws IOException
427     {
428         return endPoint.fill(buffer);
429     }
430 
431     /**
432      * <p>Writes (with blocking semantic) the given buffer of data onto the given endPoint.</p>
433      *
434      * @param endPoint the endPoint to write to
435      * @param buffer   the buffer to write
436      * @param context  the context information related to the connection
437      * @throws IOException if the buffer cannot be written
438      * @return the number of bytes written
439      */
440     protected int write(EndPoint endPoint, Buffer buffer, ConcurrentMap<String, Object> context) throws IOException
441     {
442         if (buffer == null)
443             return 0;
444 
445         int length = buffer.length();
446         final StringBuilder debug = LOG.isDebugEnabled()?new StringBuilder():null;
447         int flushed = endPoint.flush(buffer);
448         if (debug!=null)
449             debug.append(flushed);
450         
451         // Loop until all written
452         while (buffer.length()>0 && !endPoint.isOutputShutdown())
453         {
454             if (!endPoint.isBlocking())
455             {
456                 boolean ready = endPoint.blockWritable(getWriteTimeout());
457                 if (!ready)
458                     throw new IOException("Write timeout");
459             }
460             flushed = endPoint.flush(buffer);
461             if (debug!=null)
462                 debug.append("+").append(flushed);
463         }
464        
465         LOG.debug("Written {}/{} bytes {}", debug, length, endPoint);
466         buffer.compact();
467         return length;
468     }
469 
470     private class Manager extends SelectorManager
471     {
472         @Override
473         protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey key) throws IOException
474         {
475             SelectChannelEndPoint endp = new SelectChannelEndPoint(channel, selectSet, key, channel.socket().getSoTimeout());
476             endp.setConnection(selectSet.getManager().newConnection(channel,endp, key.attachment()));
477             endp.setMaxIdleTime(_writeTimeout);
478             return endp;
479         }
480 
481         @Override
482         public AsyncConnection newConnection(SocketChannel channel, AsyncEndPoint endpoint, Object attachment)
483         {
484             ProxyToServerConnection proxyToServer = (ProxyToServerConnection)attachment;
485             proxyToServer.setTimeStamp(System.currentTimeMillis());
486             proxyToServer.setEndPoint(endpoint);
487             return proxyToServer;
488         }
489 
490         @Override
491         protected void endPointOpened(SelectChannelEndPoint endpoint)
492         {
493             ProxyToServerConnection proxyToServer = (ProxyToServerConnection)endpoint.getSelectionKey().attachment();
494             proxyToServer.ready();
495         }
496 
497         @Override
498         public boolean dispatch(Runnable task)
499         {
500             return _threadPool.dispatch(task);
501         }
502 
503         @Override
504         protected void endPointClosed(SelectChannelEndPoint endpoint)
505         {
506         }
507 
508         @Override
509         protected void endPointUpgraded(ConnectedEndPoint endpoint, Connection oldConnection)
510         {
511         }
512     }
513 
514 
515 
516     public class ProxyToServerConnection implements AsyncConnection
517     {
518         private final CountDownLatch _ready = new CountDownLatch(1);
519         private final Buffer _buffer = new IndirectNIOBuffer(4096);
520         private final ConcurrentMap<String, Object> _context;
521         private volatile Buffer _data;
522         private volatile ClientToProxyConnection _toClient;
523         private volatile long _timestamp;
524         private volatile AsyncEndPoint _endPoint;
525 
526         public ProxyToServerConnection(ConcurrentMap<String, Object> context, Buffer data)
527         {
528             _context = context;
529             _data = data;
530         }
531 
532         @Override
533         public String toString()
534         {
535             StringBuilder builder = new StringBuilder("ProxyToServer");
536             builder.append("(:").append(_endPoint.getLocalPort());
537             builder.append("<=>:").append(_endPoint.getRemotePort());
538             return builder.append(")").toString();
539         }
540 
541         public Connection handle() throws IOException
542         {
543             LOG.debug("{}: begin reading from server", this);
544             try
545             {
546                 writeData();
547 
548                 while (true)
549                 {
550                     int read = read(_endPoint, _buffer, _context);
551 
552                     if (read == -1)
553                     {
554                         LOG.debug("{}: server closed connection {}", this, _endPoint);
555 
556                         if (_endPoint.isOutputShutdown() || !_endPoint.isOpen())
557                             closeClient();
558                         else
559                             _toClient.shutdownOutput();
560 
561                         break;
562                     }
563 
564                     if (read == 0)
565                         break;
566 
567                     LOG.debug("{}: read from server {} bytes {}", this, read, _endPoint);
568                     int written = write(_toClient._endPoint, _buffer, _context);
569                     LOG.debug("{}: written to {} {} bytes", this, _toClient, written);
570                 }
571                 return this;
572             }
573             catch (ClosedChannelException x)
574             {
575                 LOG.debug(x);
576                 throw x;
577             }
578             catch (IOException x)
579             {
580                 LOG.warn(this + ": unexpected exception", x);
581                 close();
582                 throw x;
583             }
584             catch (RuntimeException x)
585             {
586                 LOG.warn(this + ": unexpected exception", x);
587                 close();
588                 throw x;
589             }
590             finally
591             {
592                 LOG.debug("{}: end reading from server", this);
593             }
594         }
595 
596         public void onInputShutdown() throws IOException
597         {
598             // TODO
599         }
600 
601         private void writeData() throws IOException
602         {
603             // This method is called from handle() and closeServer()
604             // which may happen concurrently (e.g. a client closing
605             // while reading from the server), so needs synchronization
606             synchronized (this)
607             {
608                 if (_data != null)
609                 {
610                     try
611                     {
612                         int written = write(_endPoint, _data, _context);
613                         LOG.debug("{}: written to server {} bytes", this, written);
614                     }
615                     finally
616                     {
617                         // Attempt once to write the data; if the write fails (for example
618                         // because the connection is already closed), clear the data and
619                         // give up to avoid to continue to write data to a closed connection
620                         _data = null;
621                     }
622                 }
623             }
624         }
625 
626         public void setConnection(ClientToProxyConnection connection)
627         {
628             _toClient = connection;
629         }
630 
631         public long getTimeStamp()
632         {
633             return _timestamp;
634         }
635 
636         public void setTimeStamp(long timestamp)
637         {
638             _timestamp = timestamp;
639         }
640 
641         public void setEndPoint(AsyncEndPoint endpoint)
642         {
643             _endPoint = endpoint;
644         }
645 
646         public boolean isIdle()
647         {
648             return false;
649         }
650 
651         public boolean isSuspended()
652         {
653             return false;
654         }
655 
656         public void onClose()
657         {
658         }
659 
660         public void ready()
661         {
662             _ready.countDown();
663         }
664 
665         public void waitReady(long timeout) throws IOException
666         {
667             try
668             {
669                 _ready.await(timeout, TimeUnit.MILLISECONDS);
670             }
671             catch (final InterruptedException x)
672             {
673                 throw new IOException()
674                 {{
675                         initCause(x);
676                     }};
677             }
678         }
679 
680         public void closeClient() throws IOException
681         {
682             _toClient.closeClient();
683         }
684 
685         public void closeServer() throws IOException
686         {
687             _endPoint.close();
688         }
689 
690         public void close()
691         {
692             try
693             {
694                 closeClient();
695             }
696             catch (IOException x)
697             {
698                 LOG.debug(this + ": unexpected exception closing the client", x);
699             }
700 
701             try
702             {
703                 closeServer();
704             }
705             catch (IOException x)
706             {
707                 LOG.debug(this + ": unexpected exception closing the server", x);
708             }
709         }
710 
711         public void shutdownOutput() throws IOException
712         {
713             writeData();
714             _endPoint.shutdownOutput();
715         }
716 
717         public void onIdleExpired(long idleForMs)
718         {
719             try
720             {
721                 shutdownOutput();
722             }
723             catch(Exception e)
724             {
725                 LOG.debug(e);
726                 close();
727             }
728         }
729     }
730 
731     public class ClientToProxyConnection implements AsyncConnection
732     {
733         private final Buffer _buffer = new IndirectNIOBuffer(4096);
734         private final ConcurrentMap<String, Object> _context;
735         private final SocketChannel _channel;
736         private final EndPoint _endPoint;
737         private final long _timestamp;
738         private volatile ProxyToServerConnection _toServer;
739         private boolean _firstTime = true;
740 
741         public ClientToProxyConnection(ConcurrentMap<String, Object> context, SocketChannel channel, EndPoint endPoint, long timestamp)
742         {
743             _context = context;
744             _channel = channel;
745             _endPoint = endPoint;
746             _timestamp = timestamp;
747         }
748 
749         @Override
750         public String toString()
751         {
752             StringBuilder builder = new StringBuilder("ClientToProxy");
753             builder.append("(:").append(_endPoint.getLocalPort());
754             builder.append("<=>:").append(_endPoint.getRemotePort());
755             return builder.append(")").toString();
756         }
757 
758         public Connection handle() throws IOException
759         {
760             LOG.debug("{}: begin reading from client", this);
761             try
762             {
763                 if (_firstTime)
764                 {
765                     _firstTime = false;
766                     register(_channel, _toServer);
767                     LOG.debug("{}: registered channel {} with connection {}", this, _channel, _toServer);
768                 }
769 
770                 while (true)
771                 {
772                     int read = read(_endPoint, _buffer, _context);
773 
774                     if (read == -1)
775                     {
776                         LOG.debug("{}: client closed connection {}", this, _endPoint);
777 
778                         if (_endPoint.isOutputShutdown() || !_endPoint.isOpen())
779                             closeServer();
780                         else
781                             _toServer.shutdownOutput();
782 
783                         break;
784                     }
785 
786                     if (read == 0)
787                         break;
788 
789                     LOG.debug("{}: read from client {} bytes {}", this, read, _endPoint);
790                     int written = write(_toServer._endPoint, _buffer, _context);
791                     LOG.debug("{}: written to {} {} bytes", this, _toServer, written);
792                 }
793                 return this;
794             }
795             catch (ClosedChannelException x)
796             {
797                 LOG.debug(x);
798                 closeServer();
799                 throw x;
800             }
801             catch (IOException x)
802             {
803                 LOG.warn(this + ": unexpected exception", x);
804                 close();
805                 throw x;
806             }
807             catch (RuntimeException x)
808             {
809                 LOG.warn(this + ": unexpected exception", x);
810                 close();
811                 throw x;
812             }
813             finally
814             {
815                 LOG.debug("{}: end reading from client", this);
816             }
817         }
818 
819         public void onInputShutdown() throws IOException
820         {
821             // TODO
822         }
823 
824         public long getTimeStamp()
825         {
826             return _timestamp;
827         }
828 
829         public boolean isIdle()
830         {
831             return false;
832         }
833 
834         public boolean isSuspended()
835         {
836             return false;
837         }
838 
839         public void onClose()
840         {
841         }
842 
843         public void setConnection(ProxyToServerConnection connection)
844         {
845             _toServer = connection;
846         }
847 
848         public void closeClient() throws IOException
849         {
850             _endPoint.close();
851         }
852 
853         public void closeServer() throws IOException
854         {
855             _toServer.closeServer();
856         }
857 
858         public void close()
859         {
860             try
861             {
862                 closeClient();
863             }
864             catch (IOException x)
865             {
866                 LOG.debug(this + ": unexpected exception closing the client", x);
867             }
868 
869             try
870             {
871                 closeServer();
872             }
873             catch (IOException x)
874             {
875                 LOG.debug(this + ": unexpected exception closing the server", x);
876             }
877         }
878 
879         public void shutdownOutput() throws IOException
880         {
881             _endPoint.shutdownOutput();
882         }
883 
884         public void onIdleExpired(long idleForMs)
885         {
886             try
887             {
888                 shutdownOutput();
889             }
890             catch(Exception e)
891             {
892                 LOG.debug(e);
893                 close();
894             }
895         }
896     }
897 
898     /**
899      * Add a whitelist entry to an existing handler configuration
900      *
901      * @param entry new whitelist entry
902      */
903     public void addWhite(String entry)
904     {
905         add(entry, _white);
906     }
907 
908     /**
909      * Add a blacklist entry to an existing handler configuration
910      *
911      * @param entry new blacklist entry
912      */
913     public void addBlack(String entry)
914     {
915         add(entry, _black);
916     }
917 
918     /**
919      * Re-initialize the whitelist of existing handler object
920      *
921      * @param entries array of whitelist entries
922      */
923     public void setWhite(String[] entries)
924     {
925         set(entries, _white);
926     }
927 
928     /**
929      * Re-initialize the blacklist of existing handler object
930      *
931      * @param entries array of blacklist entries
932      */
933     public void setBlack(String[] entries)
934     {
935         set(entries, _black);
936     }
937 
938     /**
939      * Helper method to process a list of new entries and replace
940      * the content of the specified host map
941      *
942      * @param entries new entries
943      * @param hostMap target host map
944      */
945     protected void set(String[] entries, HostMap<String> hostMap)
946     {
947         hostMap.clear();
948 
949         if (entries != null && entries.length > 0)
950         {
951             for (String addrPath : entries)
952             {
953                 add(addrPath, hostMap);
954             }
955         }
956     }
957 
958     /**
959      * Helper method to process the new entry and add it to
960      * the specified host map.
961      *
962      * @param entry      new entry
963      * @param hostMap target host map
964      */
965     private void add(String entry, HostMap<String> hostMap)
966     {
967         if (entry != null && entry.length() > 0)
968         {
969             entry = entry.trim();
970             if (hostMap.get(entry) == null)
971             {
972                 hostMap.put(entry, entry);
973             }
974         }
975     }
976 
977     /**
978      * Check the request hostname against white- and blacklist.
979      *
980      * @param host hostname to check
981      * @return true if hostname is allowed to be proxied
982      */
983     public boolean validateDestination(String host)
984     {
985         if (_white.size() > 0)
986         {
987             Object whiteObj = _white.getLazyMatches(host);
988             if (whiteObj == null)
989             {
990                 return false;
991             }
992         }
993 
994         if (_black.size() > 0)
995         {
996             Object blackObj = _black.getLazyMatches(host);
997             if (blackObj != null)
998             {
999                 return false;
1000             }
1001         }
1002 
1003         return true;
1004     }
1005 
1006     @Override
1007     public void dump(Appendable out, String indent) throws IOException
1008     {
1009         dumpThis(out);
1010         if (_privateThreadPool)
1011             dump(out, indent, Arrays.asList(_threadPool, _selectorManager), TypeUtil.asList(getHandlers()), getBeans());
1012         else
1013             dump(out, indent, Arrays.asList(_selectorManager), TypeUtil.asList(getHandlers()), getBeans());
1014     }
1015 }