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