View Javadoc

1   // ========================================================================
2   // Copyright (c) 2006-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses.
12  // ========================================================================
13  
14  package org.eclipse.jetty.servlets;
15  
16  
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.OutputStream;
20  import java.net.InetSocketAddress;
21  import java.net.MalformedURLException;
22  import java.net.Socket;
23  import java.net.URI;
24  import java.net.URISyntaxException;
25  import java.util.Collections;
26  import java.util.Enumeration;
27  import java.util.HashSet;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.StringTokenizer;
31  
32  import javax.servlet.Servlet;
33  import javax.servlet.ServletConfig;
34  import javax.servlet.ServletContext;
35  import javax.servlet.ServletException;
36  import javax.servlet.ServletRequest;
37  import javax.servlet.ServletResponse;
38  import javax.servlet.UnavailableException;
39  import javax.servlet.http.HttpServletRequest;
40  import javax.servlet.http.HttpServletResponse;
41  
42  import org.eclipse.jetty.client.HttpClient;
43  import org.eclipse.jetty.client.HttpExchange;
44  import org.eclipse.jetty.continuation.Continuation;
45  import org.eclipse.jetty.continuation.ContinuationSupport;
46  import org.eclipse.jetty.http.HttpHeaderValues;
47  import org.eclipse.jetty.http.HttpHeaders;
48  import org.eclipse.jetty.http.HttpSchemes;
49  import org.eclipse.jetty.http.HttpURI;
50  import org.eclipse.jetty.http.PathMap;
51  import org.eclipse.jetty.io.Buffer;
52  import org.eclipse.jetty.io.EofException;
53  import org.eclipse.jetty.util.HostMap;
54  import org.eclipse.jetty.util.IO;
55  import org.eclipse.jetty.util.log.Log;
56  import org.eclipse.jetty.util.log.Logger;
57  import org.eclipse.jetty.util.thread.QueuedThreadPool;
58  
59  
60  /**
61   * Asynchronous Proxy Servlet.
62   *
63   * Forward requests to another server either as a standard web proxy (as defined by
64   * RFC2616) or as a transparent proxy.
65   * <p>
66   * This servlet needs the jetty-util and jetty-client classes to be available to
67   * the web application.
68   * <p>
69   * To facilitate JMX monitoring, the "HttpClient", it's "ThreadPool" and the "Logger"
70   * are set as context attributes prefixed with the servlet name.
71   * <p>
72   * The following init parameters may be used to configure the servlet: <ul>
73   * <li>name - Name of Proxy servlet (default: "ProxyServlet"
74   * <li>maxThreads - maximum threads
75   * <li>maxConnections - maximum connections per destination
76   * <li>HostHeader - Force the host header to a particular value
77   * <li>whiteList - comma-separated list of allowed proxy destinations
78   * <li>blackList - comma-separated list of forbidden proxy destinations
79   * </ul>
80   */
81  public class ProxyServlet implements Servlet
82  {
83      protected Logger _log;
84      protected HttpClient _client;
85      protected String _hostHeader;
86  
87      protected HashSet<String> _DontProxyHeaders = new HashSet<String>();
88      {
89          _DontProxyHeaders.add("proxy-connection");
90          _DontProxyHeaders.add("connection");
91          _DontProxyHeaders.add("keep-alive");
92          _DontProxyHeaders.add("transfer-encoding");
93          _DontProxyHeaders.add("te");
94          _DontProxyHeaders.add("trailer");
95          _DontProxyHeaders.add("proxy-authorization");
96          _DontProxyHeaders.add("proxy-authenticate");
97          _DontProxyHeaders.add("upgrade");
98      }
99  
100     protected ServletConfig _config;
101     protected ServletContext _context;
102     protected HostMap<PathMap> _white = new HostMap<PathMap>();
103     protected HostMap<PathMap> _black = new HostMap<PathMap>();
104 
105     /* ------------------------------------------------------------ */
106     /* (non-Javadoc)
107      * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
108      */
109     public void init(ServletConfig config) throws ServletException
110     {
111         _config=config;
112         _context=config.getServletContext();
113 
114         _client=new HttpClient();
115         _client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
116 
117         _hostHeader=config.getInitParameter("HostHeader");
118 
119 
120         try
121         {
122             _log= Log.getLogger("org.eclipse.jetty.servlets."+config.getServletName());
123 
124             String t = config.getInitParameter("maxThreads");
125             if (t!=null)
126                 _client.setThreadPool(new QueuedThreadPool(Integer.parseInt(t)));
127             else
128                 _client.setThreadPool(new QueuedThreadPool());
129             ((QueuedThreadPool)_client.getThreadPool()).setName(config.getServletName());
130 
131             t = config.getInitParameter("maxConnections");
132             if (t!=null)
133                 _client.setMaxConnectionsPerAddress(Integer.parseInt(t));
134 
135             _client.start();
136 
137             if (_context!=null)
138             {
139                 _context.setAttribute(config.getServletName()+".Logger",_log);
140                 _context.setAttribute(config.getServletName()+".ThreadPool",_client.getThreadPool());
141                 _context.setAttribute(config.getServletName()+".HttpClient",_client);
142             }
143             
144             String white = config.getInitParameter("whiteList");
145             if (white != null)
146             {
147                 parseList(white, _white);
148             }
149             String black = config.getInitParameter("blackList");
150             if (black != null)
151             {
152                 parseList(black, _black);
153             }
154         }
155         catch (Exception e)
156         {
157             throw new ServletException(e);
158         }
159     }
160 
161     /* ------------------------------------------------------------ */
162     /**
163      * Helper function to process a parameter value containing a list
164      * of new entries and initialize the specified host map. 
165      * 
166      * @param list comma-separated list of new entries
167      * @param hostMap target host map
168      */
169     private void parseList(String list, HostMap<PathMap> hostMap)
170     {
171         if (list != null && list.length() > 0)
172         {
173             int idx;
174             String entry;
175             
176             StringTokenizer entries = new StringTokenizer(list, ",");
177             while(entries.hasMoreTokens())
178             {
179                 entry = entries.nextToken();
180                 idx = entry.indexOf('/');
181     
182                 String host = idx > 0 ? entry.substring(0,idx) : entry;        
183                 String path = idx > 0 ? entry.substring(idx) : "/*";
184                 
185                 host = host.trim();
186                 PathMap pathMap = hostMap.get(host);
187                 if (pathMap == null)
188                 {
189                     pathMap = new PathMap(true);
190                     hostMap.put(host,pathMap);
191                 }
192                 if (path != null)
193                 {
194                     pathMap.put(path,path);
195                 }
196             }
197         }
198     }
199     
200     /* ------------------------------------------------------------ */
201     /**
202      * Check the request hostname and path against white- and blacklist.
203      * 
204      * @param host hostname to check
205      * @param path path to check
206      * @return true if request is allowed to be proxied
207      */
208     public boolean validateDestination(String host, String path)
209     {
210         if (_white.size()>0)
211         {
212             boolean match = false;
213             
214             Object whiteObj = _white.getLazyMatches(host);
215             if (whiteObj != null) 
216             {
217                 List whiteList = (whiteObj instanceof List) ? (List)whiteObj : Collections.singletonList(whiteObj);
218 
219                 for (Object entry: whiteList)
220                 {
221                     PathMap pathMap = ((Map.Entry<String, PathMap>)entry).getValue();
222                     if (match = (pathMap!=null && (pathMap.size()==0 || pathMap.match(path)!=null)))
223                         break;
224                 }
225             }
226 
227             if (!match)
228                 return false;
229         }
230 
231         if (_black.size() > 0)
232         {
233             Object blackObj = _black.getLazyMatches(host);
234             if (blackObj != null) 
235             {
236                 List blackList = (blackObj instanceof List) ? (List)blackObj : Collections.singletonList(blackObj);
237     
238                 for (Object entry: blackList)
239                 {
240                     PathMap pathMap = ((Map.Entry<String, PathMap>)entry).getValue();
241                     if (pathMap!=null && (pathMap.size()==0 || pathMap.match(path)!=null))
242                         return false;
243                 }
244             }
245         }
246         
247         return true;
248     }
249 
250     /* ------------------------------------------------------------ */
251     /* (non-Javadoc)
252      * @see javax.servlet.Servlet#getServletConfig()
253      */
254     public ServletConfig getServletConfig()
255     {
256         return _config;
257     }
258 
259 
260     /* ------------------------------------------------------------ */
261     /** Get the hostHeader.
262      * @return the hostHeader
263      */
264     public String getHostHeader()
265     {
266         return _hostHeader;
267     }
268 
269     /* ------------------------------------------------------------ */
270     /** Set the hostHeader.
271      * @param hostHeader the hostHeader to set
272      */
273     public void setHostHeader(String hostHeader)
274     {
275         _hostHeader = hostHeader;
276     }
277 
278     /* ------------------------------------------------------------ */
279     /* (non-Javadoc)
280      * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
281      */
282     public void service(ServletRequest req, ServletResponse res) throws ServletException,
283             IOException
284     {
285         final int debug=_log.isDebugEnabled()?req.hashCode():0;
286 
287         final HttpServletRequest request = (HttpServletRequest)req;
288         final HttpServletResponse response = (HttpServletResponse)res;
289         if ("CONNECT".equalsIgnoreCase(request.getMethod()))
290         {
291             handleConnect(request,response);
292         }
293         else
294         {
295             final InputStream in=request.getInputStream();
296             final OutputStream out=response.getOutputStream();
297 
298             final Continuation continuation = ContinuationSupport.getContinuation(request);
299 
300             if (!continuation.isInitial())
301                 response.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT); // Need better test that isInitial
302             else
303             {
304                 String uri=request.getRequestURI();
305                 if (request.getQueryString()!=null)
306                     uri+="?"+request.getQueryString();
307 
308                 HttpURI url=proxyHttpURI(request.getScheme(),
309                                          request.getServerName(),
310                                          request.getServerPort(),
311                                          uri);
312 
313                 if (debug!=0)
314                     _log.debug(debug+" proxy "+uri+"-->"+url);
315 
316                 if (url==null)
317                 {
318                     response.sendError(HttpServletResponse.SC_FORBIDDEN);
319                     return;
320                 }
321 
322                 HttpExchange exchange = new HttpExchange()
323                 {
324                     protected void onRequestCommitted() throws IOException
325                     {
326                     }
327 
328                     protected void onRequestComplete() throws IOException
329                     {
330                     }
331 
332                     protected void onResponseComplete() throws IOException
333                     {
334                         if (debug!=0)
335                             _log.debug(debug+" complete");
336                         continuation.complete();
337                     }
338 
339                     protected void onResponseContent(Buffer content) throws IOException
340                     {
341                         if (debug!=0)
342                             _log.debug(debug+" content"+content.length());
343                         content.writeTo(out);
344                     }
345 
346                     protected void onResponseHeaderComplete() throws IOException
347                     {
348                     }
349 
350                     protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException
351                     {
352                         if (debug!=0)
353                             _log.debug(debug+" "+version+" "+status+" "+reason);
354 
355                         if (reason!=null && reason.length()>0)
356                             response.setStatus(status,reason.toString());
357                         else
358                             response.setStatus(status);
359                     }
360 
361                     protected void onResponseHeader(Buffer name, Buffer value) throws IOException
362                     {
363                         String s = name.toString().toLowerCase();
364                         if (!_DontProxyHeaders.contains(s) ||
365                            (HttpHeaders.CONNECTION_BUFFER.equals(name) &&
366                             HttpHeaderValues.CLOSE_BUFFER.equals(value)))
367                         {
368                             if (debug!=0)
369                                 _log.debug(debug+" "+name+": "+value);
370 
371                             response.addHeader(name.toString(),value.toString());
372                         }
373                         else if (debug!=0)
374                                 _log.debug(debug+" "+name+"! "+value);
375                     }
376 
377                     protected void onConnectionFailed(Throwable ex)
378                     {
379                         onException(ex);
380                     }
381 
382                     protected void onException(Throwable ex)
383                     {
384                         if (ex instanceof EofException)
385                         {
386                             Log.ignore(ex);
387                             return;
388                         }
389                         Log.warn(ex.toString());
390                         Log.debug(ex);
391                         if (!response.isCommitted())
392                             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
393                         continuation.complete();
394                     }
395 
396                     protected void onExpire()
397                     {
398                         if (!response.isCommitted())
399                             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
400                         continuation.complete();
401                     }
402 
403                 };
404 
405                 exchange.setScheme(HttpSchemes.HTTPS.equals(request.getScheme())?HttpSchemes.HTTPS_BUFFER:HttpSchemes.HTTP_BUFFER);
406                 exchange.setMethod(request.getMethod());
407                 exchange.setURL(url.toString());
408                 exchange.setVersion(request.getProtocol());
409 
410                 if (debug!=0)
411                     _log.debug(debug+" "+request.getMethod()+" "+url+" "+request.getProtocol());
412 
413                 // check connection header
414                 String connectionHdr = request.getHeader("Connection");
415                 if (connectionHdr!=null)
416                 {
417                     connectionHdr=connectionHdr.toLowerCase();
418                     if (connectionHdr.indexOf("keep-alive")<0  &&
419                             connectionHdr.indexOf("close")<0)
420                         connectionHdr=null;
421                 }
422 
423                 // force host
424                 if (_hostHeader!=null)
425                     exchange.setRequestHeader("Host",_hostHeader);
426 
427                 // copy headers
428                 boolean xForwardedFor=false;
429                 boolean hasContent=false;
430                 long contentLength=-1;
431                 Enumeration<?> enm = request.getHeaderNames();
432                 while (enm.hasMoreElements())
433                 {
434                     // TODO could be better than this!
435                     String hdr=(String)enm.nextElement();
436                     String lhdr=hdr.toLowerCase();
437 
438                     if (_DontProxyHeaders.contains(lhdr))
439                         continue;
440                     if (connectionHdr!=null && connectionHdr.indexOf(lhdr)>=0)
441                         continue;
442                     if (_hostHeader!=null && "host".equals(lhdr))
443                         continue;
444 
445                     if ("content-type".equals(lhdr))
446                         hasContent=true;
447                     else if ("content-length".equals(lhdr))
448                     {
449                         contentLength=request.getContentLength();
450                         exchange.setRequestHeader(HttpHeaders.CONTENT_LENGTH,Long.toString(contentLength));
451                         if (contentLength>0)
452                             hasContent=true;
453                     }
454                     else if ("x-forwarded-for".equals(lhdr))
455                         xForwardedFor=true;
456 
457                     Enumeration<?> vals = request.getHeaders(hdr);
458                     while (vals.hasMoreElements())
459                     {
460                         String val = (String)vals.nextElement();
461                         if (val!=null)
462                         {
463                             if (debug!=0)
464                                 _log.debug(debug+" "+hdr+": "+val);
465 
466                             exchange.setRequestHeader(hdr,val);
467                         }
468                     }
469                 }
470 
471                 // Proxy headers
472                 exchange.setRequestHeader("Via","1.1 (jetty)");
473                 if (!xForwardedFor)
474                     exchange.addRequestHeader("X-Forwarded-For",
475                             request.getRemoteAddr());
476 
477                 if (hasContent)
478                     exchange.setRequestContentSource(in);
479 
480                 continuation.suspend(response);
481                 _client.send(exchange);
482 
483             }
484         }
485     }
486 
487 
488     /* ------------------------------------------------------------ */
489     public void handleConnect(HttpServletRequest request,
490                               HttpServletResponse response)
491         throws IOException
492     {
493         String uri = request.getRequestURI();
494 
495         String port = "";
496         String host = "";
497 
498         int c = uri.indexOf(':');
499         if (c>=0)
500         {
501             port = uri.substring(c+1);
502             host = uri.substring(0,c);
503             if (host.indexOf('/')>0)
504                 host = host.substring(host.indexOf('/')+1);
505         }
506 
507         // TODO - make this async!
508 
509 
510         InetSocketAddress inetAddress = new InetSocketAddress (host, Integer.parseInt(port));
511 
512         //if (isForbidden(HttpMessage.__SSL_SCHEME,addrPort.getHost(),addrPort.getPort(),false))
513         //{
514         //    sendForbid(request,response,uri);
515         //}
516         //else
517         {
518             InputStream in=request.getInputStream();
519             OutputStream out=response.getOutputStream();
520 
521             Socket socket = new Socket(inetAddress.getAddress(),inetAddress.getPort());
522 
523             response.setStatus(200);
524             response.setHeader("Connection","close");
525             response.flushBuffer();
526             // TODO prevent real close!
527 
528             IO.copyThread(socket.getInputStream(),out);
529             IO.copy(in,socket.getOutputStream());
530         }
531     }
532 
533     /* ------------------------------------------------------------ */
534     protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri)
535         throws MalformedURLException
536     {
537         if (!validateDestination(serverName, uri))
538             return null;
539         
540         return new HttpURI(scheme+"://"+serverName+":"+serverPort+uri);
541     }
542 
543 
544     /* (non-Javadoc)
545      * @see javax.servlet.Servlet#getServletInfo()
546      */
547     public String getServletInfo()
548     {
549         return "Proxy Servlet";
550     }
551 
552     /* (non-Javadoc)
553      * @see javax.servlet.Servlet#destroy()
554      */
555     public void destroy()
556     {
557 
558     }
559 
560     /**
561      * Transparent Proxy.
562      *
563      * This convenience extension to ProxyServlet configures the servlet as a transparent proxy.
564      * The servlet is configured with init parameters:
565      * <ul>
566      * <li>ProxyTo - a URI like http://host:80/context to which the request is proxied.
567      * <li>Prefix - a URI prefix that is striped from the start of the forwarded URI.
568      * </ul>
569      * For example, if a request was received at /foo/bar and the ProxyTo was http://host:80/context
570      * and the Prefix was /foo, then the request would be proxied to http://host:80/context/bar
571      * 
572      */
573     public static class Transparent extends ProxyServlet
574     {
575         String _prefix;
576         String _proxyTo;
577 
578         public Transparent()
579         {
580         }
581 
582         public Transparent(String prefix, String host, int port)
583         {
584             this(prefix,"http",host,port,null);
585         }
586 
587         public Transparent(String prefix, String schema, String host, int port, String path)
588         {
589             try
590             {
591                 if (prefix != null)
592                 {
593                     _prefix = new URI(prefix).normalize().toString();
594                 }
595                 _proxyTo = new URI(schema,null,host,port,path,null,null).normalize().toString();
596             }
597             catch (URISyntaxException ex)
598             {
599                 _log.debug("Invalid URI syntax",ex);
600             }
601         }
602 
603         @Override
604         public void init(ServletConfig config) throws ServletException
605         {
606             super.init(config);
607 
608             String prefix = config.getInitParameter("Prefix");
609             _prefix = prefix == null?_prefix:prefix;
610 
611             // Adjust prefix value to account for context path
612             String contextPath = _context.getContextPath();
613             _prefix = _prefix == null?contextPath:(contextPath + _prefix);
614 
615             String proxyTo = config.getInitParameter("ProxyTo");
616             _proxyTo = proxyTo == null?_proxyTo:proxyTo;
617 
618             if (_proxyTo == null)
619                 throw new UnavailableException("ProxyTo parameter is requred.");
620 
621             if (!_prefix.startsWith("/"))
622                 throw new UnavailableException("Prefix parameter must start with a '/'.");
623 
624             _log.info(config.getServletName()+" @ " + _prefix + " to " + _proxyTo);
625         }
626 
627         @Override
628         protected HttpURI proxyHttpURI(final String scheme, final String serverName, int serverPort, final String uri) throws MalformedURLException
629         {
630             try
631             {
632                 if (!uri.startsWith(_prefix))
633                     return null;
634                 
635                 URI dstUri = new URI(_proxyTo + uri.substring(_prefix.length())).normalize();
636                 
637                 if (!validateDestination(dstUri.getHost(),dstUri.getPath()))
638                     return null;
639 
640                 return new HttpURI(dstUri.toString());
641             }
642             catch (URISyntaxException ex)
643             {
644                 throw new MalformedURLException(ex.getMessage());
645             }
646         }
647     }
648 }