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