View Javadoc

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