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