View Javadoc

1   // ========================================================================
2   // Copyright 2006-2007 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.proxy;
16  
17  
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.OutputStream;
21  import java.net.InetSocketAddress;
22  import java.net.MalformedURLException;
23  import java.net.Socket;
24  import java.net.URL;
25  import java.util.Enumeration;
26  import java.util.HashSet;
27  
28  import javax.servlet.Servlet;
29  import javax.servlet.ServletConfig;
30  import javax.servlet.ServletContext;
31  import javax.servlet.ServletException;
32  import javax.servlet.ServletRequest;
33  import javax.servlet.ServletResponse;
34  import javax.servlet.UnavailableException;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  import javax.servlet.http.HttpServletResponseWrapper;
38  
39  import org.mortbay.io.Buffer;
40  import org.mortbay.jetty.Connector;
41  import org.mortbay.jetty.Handler;
42  import org.mortbay.jetty.HttpSchemes;
43  import org.mortbay.jetty.HttpURI;
44  import org.mortbay.jetty.Server;
45  import org.mortbay.jetty.bio.SocketConnector;
46  import org.mortbay.jetty.client.Address;
47  import org.mortbay.jetty.client.HttpClient;
48  import org.mortbay.jetty.client.HttpExchange;
49  import org.mortbay.jetty.handler.ContextHandlerCollection;
50  import org.mortbay.jetty.handler.DefaultHandler;
51  import org.mortbay.jetty.handler.HandlerCollection;
52  import org.mortbay.jetty.servlet.Context;
53  import org.mortbay.jetty.servlet.ServletHolder;
54  import org.mortbay.jetty.webapp.WebAppContext;
55  import org.mortbay.util.IO;
56  import org.mortbay.util.ajax.Continuation;
57  import org.mortbay.util.ajax.ContinuationSupport;
58  
59  
60  
61  /**
62   * Asynchronous Proxy Servlet.
63   * 
64   * Forward requests to another server either as a standard web proxy (as defined by
65   * RFC2616) or as a transparent proxy.
66   * 
67   * This servlet needs the jetty-util and jetty-client classes to be available to
68   * the web application.
69   *
70   */
71  public class AsyncProxyServlet implements Servlet
72  {
73      HttpClient _client;
74  
75      protected HashSet<String> _DontProxyHeaders = new HashSet<String>();
76      {
77          _DontProxyHeaders.add("proxy-connection");
78          _DontProxyHeaders.add("connection");
79          _DontProxyHeaders.add("keep-alive");
80          _DontProxyHeaders.add("transfer-encoding");
81          _DontProxyHeaders.add("te");
82          _DontProxyHeaders.add("trailer");
83          _DontProxyHeaders.add("proxy-authorization");
84          _DontProxyHeaders.add("proxy-authenticate");
85          _DontProxyHeaders.add("upgrade");
86      }
87  
88      private ServletConfig config;
89      private ServletContext context;
90  
91      /* (non-Javadoc)
92       * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
93       */
94      public void init(ServletConfig config) throws ServletException
95      {
96          this.config=config;
97          this.context=config.getServletContext();
98  
99          _client=new HttpClient();
100         //_client.setConnectorType(HttpClient.CONNECTOR_SOCKET);
101         _client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL);
102         try
103         {
104             _client.start();
105         }
106         catch (Exception e)
107         {
108             throw new ServletException(e);
109         }
110     }
111 
112     /* (non-Javadoc)
113      * @see javax.servlet.Servlet#getServletConfig()
114      */
115     public ServletConfig getServletConfig()
116     {
117         return config;
118     }
119 
120     /* (non-Javadoc)
121      * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
122      */
123     public void service(ServletRequest req, ServletResponse res) throws ServletException,
124             IOException
125     {
126         while (res instanceof HttpServletResponseWrapper)
127             res=(HttpServletResponse)((HttpServletResponseWrapper)res).getResponse();
128            
129         final HttpServletRequest request = (HttpServletRequest)req;
130         final HttpServletResponse response = (HttpServletResponse)res;
131         
132         if ("CONNECT".equalsIgnoreCase(request.getMethod()))
133         {
134             handleConnect(request,response);
135         }
136         else
137         {
138             final InputStream in=request.getInputStream();
139             final OutputStream out=response.getOutputStream();
140             final Continuation continuation = ContinuationSupport.getContinuation(request,request);
141 
142 
143             if (!continuation.isPending())
144             {
145                 final byte[] buffer = new byte[4096]; // TODO avoid this!
146                 String uri=request.getRequestURI();
147                 if (request.getQueryString()!=null)
148                     uri+="?"+request.getQueryString();
149 
150                 HttpURI url=proxyHttpURI(request.getScheme(),
151                         request.getServerName(),
152                         request.getServerPort(),
153                         uri);
154                 
155                 if (url==null)
156                 {
157                     response.sendError(HttpServletResponse.SC_FORBIDDEN);
158                     return;
159                 }
160 
161                 HttpExchange exchange = new HttpExchange()
162                 {
163 
164                     protected void onRequestCommitted() throws IOException
165                     {
166                     }
167 
168                     protected void onRequestComplete() throws IOException
169                     {
170                     }
171 
172                     protected void onResponseComplete() throws IOException
173                     {
174                         continuation.resume();
175                     }
176 
177                     protected void onResponseContent(Buffer content) throws IOException
178                     {
179                         // TODO Avoid this copy
180                         while (content.hasContent())
181                         {
182                             int len=content.get(buffer,0,buffer.length);
183                             out.write(buffer,0,len);  // May block here for a little bit!
184                         }
185                     }
186 
187                     protected void onResponseHeaderComplete() throws IOException
188                     {
189                     }
190 
191                     protected void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException
192                     {
193                         if (reason!=null && reason.length()>0)
194                             response.setStatus(status,reason.toString());
195                         else
196                             response.setStatus(status);
197 
198                     }
199 
200                     protected void onResponseHeader(Buffer name, Buffer value) throws IOException
201                     {
202                         String s = name.toString().toLowerCase();
203                         if (!_DontProxyHeaders.contains(s))
204                             response.addHeader(name.toString(),value.toString());
205                     }
206 
207                 };
208                 
209                 exchange.setVersion(request.getProtocol());
210                 exchange.setMethod(request.getMethod());
211                 
212                 exchange.setURL(url.toString());
213                 
214                 // check connection header
215                 String connectionHdr = request.getHeader("Connection");
216                 if (connectionHdr!=null)
217                 {
218                     connectionHdr=connectionHdr.toLowerCase();
219                     if (connectionHdr.indexOf("keep-alive")<0  &&
220                             connectionHdr.indexOf("close")<0)
221                         connectionHdr=null;
222                 }
223 
224                 // copy headers
225                 boolean xForwardedFor=false;
226                 boolean hasContent=false;
227                 long contentLength=-1;
228                 Enumeration enm = request.getHeaderNames();
229                 while (enm.hasMoreElements())
230                 {
231                     // TODO could be better than this!
232                     String hdr=(String)enm.nextElement();
233                     String lhdr=hdr.toLowerCase();
234 
235                     if (_DontProxyHeaders.contains(lhdr))
236                         continue;
237                     if (connectionHdr!=null && connectionHdr.indexOf(lhdr)>=0)
238                         continue;
239 
240                     if ("content-type".equals(lhdr))
241                         hasContent=true;
242                     if ("content-length".equals(lhdr))
243                         contentLength=request.getContentLength();
244 
245                     Enumeration vals = request.getHeaders(hdr);
246                     while (vals.hasMoreElements())
247                     {
248                         String val = (String)vals.nextElement();
249                         if (val!=null)
250                         {
251                             exchange.setRequestHeader(lhdr,val);
252                             xForwardedFor|="X-Forwarded-For".equalsIgnoreCase(hdr);
253                         }
254                     }
255                 }
256 
257                 // Proxy headers
258                 exchange.setRequestHeader("Via","1.1 (jetty)");
259                 if (!xForwardedFor)
260                     exchange.addRequestHeader("X-Forwarded-For",
261                             request.getRemoteAddr());
262 
263                 if (hasContent)
264                     exchange.setRequestContentSource(in);
265 
266                 _client.send(exchange);
267 
268                 continuation.suspend(30000);
269             }
270         }
271     }
272 
273 
274     /* ------------------------------------------------------------ */
275     /**
276     /** Resolve requested URL to the Proxied HttpURI
277      * @param scheme The scheme of the received request.
278      * @param serverName The server encoded in the received request(which 
279      * may be from an absolute URL in the request line).
280      * @param serverPort The server port of the received request (which 
281      * may be from an absolute URL in the request line).
282      * @param uri The URI of the received request.
283      * @return The HttpURI to which the request should be proxied.
284      * @throws MalformedURLException
285      */
286     protected HttpURI proxyHttpURI(String scheme, String serverName, int serverPort, String uri)
287         throws MalformedURLException
288     {
289         return new HttpURI(scheme+"://"+serverName+":"+serverPort+uri);
290     }
291 
292     /* ------------------------------------------------------------ */
293     public void handleConnect(HttpServletRequest request,
294                               HttpServletResponse response)
295         throws IOException
296     {
297         String uri = request.getRequestURI();
298 
299         String port = "";
300         String host = "";
301 
302         int c = uri.indexOf(':');
303         if (c>=0)
304         {
305             port = uri.substring(c+1);
306             host = uri.substring(0,c);
307             if (host.indexOf('/')>0)
308                 host = host.substring(host.indexOf('/')+1);
309         }
310 
311         InetSocketAddress inetAddress = new InetSocketAddress (host, Integer.parseInt(port));
312 
313         //if (isForbidden(HttpMessage.__SSL_SCHEME,addrPort.getHost(),addrPort.getPort(),false))
314         //{
315         //    sendForbid(request,response,uri);
316         //}
317         //else
318         {
319             InputStream in=request.getInputStream();
320             OutputStream out=response.getOutputStream();
321 
322             Socket socket = new Socket(inetAddress.getAddress(),inetAddress.getPort());
323 
324             response.setStatus(200);
325             response.setHeader("Connection","close");
326             response.flushBuffer();
327 
328 
329 
330             IO.copyThread(socket.getInputStream(),out);
331             IO.copy(in,socket.getOutputStream());
332         }
333     }
334 
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 AsyncProxyServlet
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         
394         protected HttpURI proxyHttpURI(final String scheme, final String serverName, int serverPort, final String uri) throws MalformedURLException
395         {
396             if (_prefix!=null && !uri.startsWith(_prefix))
397                 return null;
398 
399             if (_prefix!=null)
400                 return new HttpURI(_proxyTo+uri.substring(_prefix.length()));
401             return new HttpURI(_proxyTo+uri);
402         }
403     }
404 }