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