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.Enumeration;
26  import java.util.HashSet;
27  import javax.servlet.Servlet;
28  import javax.servlet.ServletConfig;
29  import javax.servlet.ServletContext;
30  import javax.servlet.ServletException;
31  import javax.servlet.ServletRequest;
32  import javax.servlet.ServletResponse;
33  import javax.servlet.UnavailableException;
34  import javax.servlet.http.HttpServletRequest;
35  import javax.servlet.http.HttpServletResponse;
36  
37  import org.eclipse.jetty.client.HttpClient;
38  import org.eclipse.jetty.client.HttpExchange;
39  import org.eclipse.jetty.continuation.Continuation;
40  import org.eclipse.jetty.continuation.ContinuationSupport;
41  import org.eclipse.jetty.http.HttpHeaderValues;
42  import org.eclipse.jetty.http.HttpHeaders;
43  import org.eclipse.jetty.http.HttpSchemes;
44  import org.eclipse.jetty.http.HttpURI;
45  import org.eclipse.jetty.io.Buffer;
46  import org.eclipse.jetty.io.EofException;
47  import org.eclipse.jetty.util.IO;
48  import org.eclipse.jetty.util.log.Log;
49  import org.eclipse.jetty.util.log.Logger;
50  import org.eclipse.jetty.util.thread.QueuedThreadPool;
51  
52  
53  /**
54   * Asynchronous Proxy Servlet.
55   *
56   * Forward requests to another server either as a standard web proxy (as defined by
57   * RFC2616) or as a transparent proxy.
58   * <p>
59   * This servlet needs the jetty-util and jetty-client classes to be available to
60   * the web application.
61   * <p>
62   * To facilitate JMX monitoring, the "HttpClient", it's "ThreadPool" and the "Logger"
63   * are set as context attributes prefixed with "org.eclipse.jetty.servlets."+name
64   * (unless otherwise set with attrPrefix). This attribute prefix is also used for the
65   * logger name.
66   * <p>
67   * The following init parameters may be used to configure the servlet: <ul>
68   * <li>name - Name of Proxy servlet (default: "ProxyServlet"
69   * <li>maxThreads - maximum threads
70   * <li>maxConnections - maximum connections per destination
71   * <li>HostHeader - Force the host header to a particular value
72   * </ul>
73   */
74  public class ProxyServlet implements Servlet
75  {
76      protected Logger _log;
77      HttpClient _client;
78      String _hostHeader;
79  
80      protected HashSet<String> _DontProxyHeaders = new HashSet<String>();
81      {
82          _DontProxyHeaders.add("proxy-connection");
83          _DontProxyHeaders.add("connection");
84          _DontProxyHeaders.add("keep-alive");
85          _DontProxyHeaders.add("transfer-encoding");
86          _DontProxyHeaders.add("te");
87          _DontProxyHeaders.add("trailer");
88          _DontProxyHeaders.add("proxy-authorization");
89          _DontProxyHeaders.add("proxy-authenticate");
90          _DontProxyHeaders.add("upgrade");
91      }
92  
93      protected ServletConfig _config;
94      protected ServletContext _context;
95      protected String _name="ProxyServlet";
96  
97      /* ------------------------------------------------------------ */
98      /* (non-Javadoc)
99       * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
100      */
101     public void init(ServletConfig config) throws ServletException
102     {
103         _config=config;
104         _context=config.getServletContext();
105 
106         _client=new HttpClient();
107         _client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
108 
109         _hostHeader=config.getInitParameter("HostHeader");
110 
111 
112         try
113         {
114             String t = config.getInitParameter("attrPrefix");
115             if (t!=null)
116                 _name=t;
117             _log= Log.getLogger("org.eclipse.jetty.servlets."+_name);
118 
119             t = config.getInitParameter("maxThreads");
120             if (t!=null)
121                 _client.setThreadPool(new QueuedThreadPool(Integer.parseInt(t)));
122             else
123                 _client.setThreadPool(new QueuedThreadPool());
124             ((QueuedThreadPool)_client.getThreadPool()).setName(_name.substring(_name.lastIndexOf('.')+1));
125 
126             t = config.getInitParameter("maxConnections");
127             if (t!=null)
128                 _client.setMaxConnectionsPerAddress(Integer.parseInt(t));
129 
130             _client.start();
131 
132             if (_context!=null)
133             {
134                 _context.setAttribute("org.eclipse.jetty.servlets."+_name+".Logger",_log);
135                 _context.setAttribute("org.eclipse.jetty.servlets."+_name+".ThreadPool",_client.getThreadPool());
136                 _context.setAttribute("org.eclipse.jetty.servlets."+_name+".HttpClient",_client);
137             }
138         }
139         catch (Exception e)
140         {
141             throw new ServletException(e);
142         }
143     }
144 
145     /* ------------------------------------------------------------ */
146     /* (non-Javadoc)
147      * @see javax.servlet.Servlet#getServletConfig()
148      */
149     public ServletConfig getServletConfig()
150     {
151         return _config;
152     }
153 
154 
155     /* ------------------------------------------------------------ */
156     /** Get the hostHeader.
157      * @return the hostHeader
158      */
159     public String getHostHeader()
160     {
161         return _hostHeader;
162     }
163 
164     /* ------------------------------------------------------------ */
165     /** Set the hostHeader.
166      * @param hostHeader the hostHeader to set
167      */
168     public void setHostHeader(String hostHeader)
169     {
170         _hostHeader = hostHeader;
171     }
172 
173     /* ------------------------------------------------------------ */
174     /* (non-Javadoc)
175      * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
176      */
177     public void service(ServletRequest req, ServletResponse res) throws ServletException,
178             IOException
179     {
180         final int debug=_log.isDebugEnabled()?req.hashCode():0;
181 
182         final HttpServletRequest request = (HttpServletRequest)req;
183         final HttpServletResponse response = (HttpServletResponse)res;
184         if ("CONNECT".equalsIgnoreCase(request.getMethod()))
185         {
186             handleConnect(request,response);
187         }
188         else
189         {
190             final InputStream in=request.getInputStream();
191             final OutputStream out=response.getOutputStream();
192 
193             final Continuation continuation = ContinuationSupport.getContinuation(request);
194 
195             if (!continuation.isInitial())
196                 response.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT); // Need better test that isInitial
197             else
198             {
199                 String uri=request.getRequestURI();
200                 if (request.getQueryString()!=null)
201                     uri+="?"+request.getQueryString();
202 
203                 HttpURI url=proxyHttpURI(request.getScheme(),
204                                          request.getServerName(),
205                                          request.getServerPort(),
206                                          uri);
207 
208                 if (debug!=0)
209                     _log.debug(debug+" proxy "+uri+"-->"+url);
210 
211                 if (url==null)
212                 {
213                     response.sendError(HttpServletResponse.SC_FORBIDDEN);
214                     return;
215                 }
216 
217                 HttpExchange exchange = new HttpExchange()
218                 {
219                     protected void onRequestCommitted() throws IOException
220                     {
221                     }
222 
223                     protected void onRequestComplete() throws IOException
224                     {
225                     }
226 
227                     protected void onResponseComplete() throws IOException
228                     {
229                         if (debug!=0)
230                             _log.debug(debug+" complete");
231                         continuation.complete();
232                     }
233 
234                     protected void onResponseContent(Buffer content) throws IOException
235                     {
236                         if (debug!=0)
237                             _log.debug(debug+" content"+content.length());
238                         content.writeTo(out);
239                     }
240 
241                     protected void onResponseHeaderComplete() throws IOException
242                     {
243                     }
244 
245                     protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException
246                     {
247                         if (debug!=0)
248                             _log.debug(debug+" "+version+" "+status+" "+reason);
249 
250                         if (reason!=null && reason.length()>0)
251                             response.setStatus(status,reason.toString());
252                         else
253                             response.setStatus(status);
254                     }
255 
256                     protected void onResponseHeader(Buffer name, Buffer value) throws IOException
257                     {
258                         String s = name.toString().toLowerCase();
259                         if (!_DontProxyHeaders.contains(s) ||
260                            (HttpHeaders.CONNECTION_BUFFER.equals(name) &&
261                             HttpHeaderValues.CLOSE_BUFFER.equals(value)))
262                         {
263                             if (debug!=0)
264                                 _log.debug(debug+" "+name+": "+value);
265 
266                             response.addHeader(name.toString(),value.toString());
267                         }
268                         else if (debug!=0)
269                                 _log.debug(debug+" "+name+"! "+value);
270                     }
271 
272                     protected void onConnectionFailed(Throwable ex)
273                     {
274                         onException(ex);
275                     }
276 
277                     protected void onException(Throwable ex)
278                     {
279                         if (ex instanceof EofException)
280                         {
281                             Log.ignore(ex);
282                             return;
283                         }
284                         Log.warn(ex.toString());
285                         Log.debug(ex);
286                         if (!response.isCommitted())
287                             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
288                         continuation.complete();
289                     }
290 
291                     protected void onExpire()
292                     {
293                         if (!response.isCommitted())
294                             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
295                         continuation.complete();
296                     }
297 
298                 };
299 
300                 exchange.setScheme(HttpSchemes.HTTPS.equals(request.getScheme())?HttpSchemes.HTTPS_BUFFER:HttpSchemes.HTTP_BUFFER);
301                 exchange.setMethod(request.getMethod());
302                 exchange.setURL(url.toString());
303                 exchange.setVersion(request.getProtocol());
304 
305                 if (debug!=0)
306                     _log.debug(debug+" "+request.getMethod()+" "+url+" "+request.getProtocol());
307 
308                 // check connection header
309                 String connectionHdr = request.getHeader("Connection");
310                 if (connectionHdr!=null)
311                 {
312                     connectionHdr=connectionHdr.toLowerCase();
313                     if (connectionHdr.indexOf("keep-alive")<0  &&
314                             connectionHdr.indexOf("close")<0)
315                         connectionHdr=null;
316                 }
317 
318                 // force host
319                 if (_hostHeader!=null)
320                     exchange.setRequestHeader("Host",_hostHeader);
321 
322                 // copy headers
323                 boolean xForwardedFor=false;
324                 boolean hasContent=false;
325                 long contentLength=-1;
326                 Enumeration<?> enm = request.getHeaderNames();
327                 while (enm.hasMoreElements())
328                 {
329                     // TODO could be better than this!
330                     String hdr=(String)enm.nextElement();
331                     String lhdr=hdr.toLowerCase();
332 
333                     if (_DontProxyHeaders.contains(lhdr))
334                         continue;
335                     if (connectionHdr!=null && connectionHdr.indexOf(lhdr)>=0)
336                         continue;
337                     if (_hostHeader!=null && "host".equals(lhdr))
338                         continue;
339 
340                     if ("content-type".equals(lhdr))
341                         hasContent=true;
342                     else if ("content-length".equals(lhdr))
343                     {
344                         contentLength=request.getContentLength();
345                         exchange.setRequestHeader(HttpHeaders.CONTENT_LENGTH,Long.toString(contentLength));
346                         if (contentLength>0)
347                             hasContent=true;
348                     }
349                     else if ("x-forwarded-for".equals(lhdr))
350                         xForwardedFor=true;
351 
352                     Enumeration<?> vals = request.getHeaders(hdr);
353                     while (vals.hasMoreElements())
354                     {
355                         String val = (String)vals.nextElement();
356                         if (val!=null)
357                         {
358                             if (debug!=0)
359                                 _log.debug(debug+" "+hdr+": "+val);
360 
361                             exchange.setRequestHeader(hdr,val);
362                         }
363                     }
364                 }
365 
366                 // Proxy headers
367                 exchange.setRequestHeader("Via","1.1 (jetty)");
368                 if (!xForwardedFor)
369                     exchange.addRequestHeader("X-Forwarded-For",
370                             request.getRemoteAddr());
371 
372                 if (hasContent)
373                     exchange.setRequestContentSource(in);
374 
375                 continuation.suspend(response);
376                 _client.send(exchange);
377 
378             }
379         }
380     }
381 
382 
383     /* ------------------------------------------------------------ */
384     public void handleConnect(HttpServletRequest request,
385                               HttpServletResponse response)
386         throws IOException
387     {
388         String uri = request.getRequestURI();
389 
390         String port = "";
391         String host = "";
392 
393         int c = uri.indexOf(':');
394         if (c>=0)
395         {
396             port = uri.substring(c+1);
397             host = uri.substring(0,c);
398             if (host.indexOf('/')>0)
399                 host = host.substring(host.indexOf('/')+1);
400         }
401 
402         // TODO - make this async!
403 
404 
405         InetSocketAddress inetAddress = new InetSocketAddress (host, Integer.parseInt(port));
406 
407         //if (isForbidden(HttpMessage.__SSL_SCHEME,addrPort.getHost(),addrPort.getPort(),false))
408         //{
409         //    sendForbid(request,response,uri);
410         //}
411         //else
412         {
413             InputStream in=request.getInputStream();
414             OutputStream out=response.getOutputStream();
415 
416             Socket socket = new Socket(inetAddress.getAddress(),inetAddress.getPort());
417 
418             response.setStatus(200);
419             response.setHeader("Connection","close");
420             response.flushBuffer();
421             // TODO prevent real close!
422 
423             IO.copyThread(socket.getInputStream(),out);
424             IO.copy(in,socket.getOutputStream());
425         }
426     }
427 
428     /* ------------------------------------------------------------ */
429     protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri)
430         throws MalformedURLException
431     {
432         return new HttpURI(scheme+"://"+serverName+":"+serverPort+uri);
433     }
434 
435 
436     /* (non-Javadoc)
437      * @see javax.servlet.Servlet#getServletInfo()
438      */
439     public String getServletInfo()
440     {
441         return "Proxy Servlet";
442     }
443 
444     /* (non-Javadoc)
445      * @see javax.servlet.Servlet#destroy()
446      */
447     public void destroy()
448     {
449 
450     }
451 
452     /**
453      * Transparent Proxy.
454      *
455      * This convenience extension to ProxyServlet configures the servlet as a transparent proxy.
456      * The servlet is configured with init parameters:
457      * <ul>
458      * <li>ProxyTo - a URI like http://host:80/context to which the request is proxied.
459      * <li>Prefix - a URI prefix that is striped from the start of the forwarded URI.
460      * </ul>
461      * For example, if a request was received at /foo/bar and the ProxyTo was http://host:80/context
462      * and the Prefix was /foo, then the request would be proxied to http://host:80/context/bar
463      *
464      */
465     public static class Transparent extends ProxyServlet
466     {
467         String _prefix;
468         String _proxyTo;
469 
470         public Transparent()
471         {
472         }
473 
474         public Transparent(String prefix, String host, int port)
475         {
476             this(prefix,"http",host,port,null);
477         }
478 
479         public Transparent(String prefix, String schema, String host, int port, String path)
480         {
481             try
482             {
483                 if (prefix != null)
484                 {
485                     _prefix = new URI(prefix).normalize().toString();
486                 }
487                 _proxyTo = new URI(schema,null,host,port,path,null,null).normalize().toString();
488             }
489             catch (URISyntaxException ex)
490             {
491                 _log.debug("Invalid URI syntax",ex);
492             }
493         }
494 
495         @Override
496         public void init(ServletConfig config) throws ServletException
497         {
498             super.init(config);
499 
500             String prefix = config.getInitParameter("Prefix");
501             _prefix = prefix == null?_prefix:prefix;
502 
503             // Adjust prefix value to account for context path
504             String contextPath = _context.getContextPath();
505             _prefix = _prefix == null?contextPath:(contextPath + _prefix);
506 
507             String proxyTo = config.getInitParameter("ProxyTo");
508             _proxyTo = proxyTo == null?_proxyTo:proxyTo;
509 
510             if (_proxyTo == null)
511                 throw new UnavailableException("ProxyTo parameter is requred.");
512 
513             if (!_prefix.startsWith("/"))
514                 throw new UnavailableException("Prefix parameter must start with a '/'.");
515 
516             _log.info(_name + " @ " + _prefix + " to " + _proxyTo);
517         }
518 
519         @Override
520         protected HttpURI proxyHttpURI(final String scheme, final String serverName, int serverPort, final String uri) throws MalformedURLException
521         {
522             try
523             {
524                 if (!uri.startsWith(_prefix))
525                     return null;
526 
527                 return new HttpURI(new URI(_proxyTo + uri.substring(_prefix.length())).normalize().toString());
528             }
529             catch (URISyntaxException ex)
530             {
531                 throw new MalformedURLException(ex.getMessage());
532             }
533         }
534     }
535 }