View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.proxy;
20  
21  import java.io.IOException;
22  import java.net.InetAddress;
23  import java.net.URI;
24  import java.net.UnknownHostException;
25  import java.util.Collections;
26  import java.util.Enumeration;
27  import java.util.HashSet;
28  import java.util.Iterator;
29  import java.util.Locale;
30  import java.util.Set;
31  import java.util.concurrent.Executor;
32  import java.util.concurrent.TimeoutException;
33  
34  import javax.servlet.AsyncContext;
35  import javax.servlet.ServletConfig;
36  import javax.servlet.ServletContext;
37  import javax.servlet.ServletException;
38  import javax.servlet.UnavailableException;
39  import javax.servlet.http.HttpServlet;
40  import javax.servlet.http.HttpServletRequest;
41  import javax.servlet.http.HttpServletResponse;
42  
43  import org.eclipse.jetty.client.HttpClient;
44  import org.eclipse.jetty.client.api.Request;
45  import org.eclipse.jetty.client.api.Response;
46  import org.eclipse.jetty.http.HttpField;
47  import org.eclipse.jetty.http.HttpHeader;
48  import org.eclipse.jetty.http.HttpHeaderValue;
49  import org.eclipse.jetty.http.HttpStatus;
50  import org.eclipse.jetty.util.HttpCookieStore;
51  import org.eclipse.jetty.util.log.Log;
52  import org.eclipse.jetty.util.log.Logger;
53  import org.eclipse.jetty.util.thread.QueuedThreadPool;
54  
55  /**
56   * <p>Abstract base class for proxy servlets.</p>
57   * <p>Forwards requests to another server either as a standard web reverse
58   * proxy or as a transparent reverse proxy (as defined by RFC 7230).</p>
59   * <p>To facilitate JMX monitoring, the {@link HttpClient} instance is set
60   * as ServletContext attribute, prefixed with this servlet's name and
61   * exposed by the mechanism provided by
62   * {@link ServletContext#setAttribute(String, Object)}.</p>
63   * <p>The following init parameters may be used to configure the servlet:</p>
64   * <ul>
65   * <li>preserveHost - the host header specified by the client is forwarded to the server</li>
66   * <li>hostHeader - forces the host header to a particular value</li>
67   * <li>viaHost - the name to use in the Via header: Via: http/1.1 &lt;viaHost&gt;</li>
68   * <li>whiteList - comma-separated list of allowed proxy hosts</li>
69   * <li>blackList - comma-separated list of forbidden proxy hosts</li>
70   * </ul>
71   * <p>In addition, see {@link #createHttpClient()} for init parameters
72   * used to configure the {@link HttpClient} instance.</p>
73   * <p>NOTE: By default the Host header sent to the server by this proxy
74   * servlet is the server's host name. However, this breaks redirects.
75   * Set {@code preserveHost} to {@code true} to make redirects working,
76   * although this may break server's virtual host selection.</p>
77   * <p>The default behavior of not preserving the Host header mimics
78   * the default behavior of Apache httpd and Nginx, which both have
79   * a way to be configured to preserve the Host header.</p>
80   */
81  public abstract class AbstractProxyServlet extends HttpServlet
82  {
83      protected static final Set<String> HOP_HEADERS;
84      static
85      {
86          Set<String> hopHeaders = new HashSet<>();
87          hopHeaders.add("connection");
88          hopHeaders.add("keep-alive");
89          hopHeaders.add("proxy-authorization");
90          hopHeaders.add("proxy-authenticate");
91          hopHeaders.add("proxy-connection");
92          hopHeaders.add("transfer-encoding");
93          hopHeaders.add("te");
94          hopHeaders.add("trailer");
95          hopHeaders.add("upgrade");
96          HOP_HEADERS = Collections.unmodifiableSet(hopHeaders);
97      }
98  
99      private final Set<String> _whiteList = new HashSet<>();
100     private final Set<String> _blackList = new HashSet<>();
101     protected Logger _log;
102     private boolean _preserveHost;
103     private String _hostHeader;
104     private String _viaHost;
105     private HttpClient _client;
106     private long _timeout;
107 
108     @Override
109     public void init() throws ServletException
110     {
111         _log = createLogger();
112 
113         ServletConfig config = getServletConfig();
114 
115         _preserveHost = Boolean.parseBoolean(config.getInitParameter("preserveHost"));
116 
117         _hostHeader = config.getInitParameter("hostHeader");
118 
119         _viaHost = config.getInitParameter("viaHost");
120         if (_viaHost == null)
121             _viaHost = viaHost();
122 
123         try
124         {
125             _client = createHttpClient();
126 
127             // Put the HttpClient in the context to leverage ContextHandler.MANAGED_ATTRIBUTES
128             getServletContext().setAttribute(config.getServletName() + ".HttpClient", _client);
129 
130             String whiteList = config.getInitParameter("whiteList");
131             if (whiteList != null)
132                 getWhiteListHosts().addAll(parseList(whiteList));
133 
134             String blackList = config.getInitParameter("blackList");
135             if (blackList != null)
136                 getBlackListHosts().addAll(parseList(blackList));
137         }
138         catch (Exception e)
139         {
140             throw new ServletException(e);
141         }
142     }
143 
144     @Override
145     public void destroy()
146     {
147         try
148         {
149             _client.stop();
150         }
151         catch (Exception x)
152         {
153             if (_log.isDebugEnabled())
154                 _log.debug(x);
155         }
156     }
157 
158     public String getHostHeader()
159     {
160         return _hostHeader;
161     }
162 
163     public String getViaHost()
164     {
165         return _viaHost;
166     }
167 
168     private static String viaHost()
169     {
170         try
171         {
172             return InetAddress.getLocalHost().getHostName();
173         }
174         catch (UnknownHostException x)
175         {
176             return "localhost";
177         }
178     }
179 
180     public long getTimeout()
181     {
182         return _timeout;
183     }
184 
185     public void setTimeout(long timeout)
186     {
187         this._timeout = timeout;
188     }
189 
190     public Set<String> getWhiteListHosts()
191     {
192         return _whiteList;
193     }
194 
195     public Set<String> getBlackListHosts()
196     {
197         return _blackList;
198     }
199 
200     /**
201      * @return a logger instance with a name derived from this servlet's name.
202      */
203     protected Logger createLogger()
204     {
205         String servletName = getServletConfig().getServletName();
206         servletName = servletName.replace('-', '.');
207         if ((getClass().getPackage() != null) && !servletName.startsWith(getClass().getPackage().getName()))
208         {
209             servletName = getClass().getName() + "." + servletName;
210         }
211         return Log.getLogger(servletName);
212     }
213 
214     /**
215      * <p>Creates a {@link HttpClient} instance, configured with init parameters of this servlet.</p>
216      * <p>The init parameters used to configure the {@link HttpClient} instance are:</p>
217      * <table>
218      * <thead>
219      * <tr>
220      * <th>init-param</th>
221      * <th>default</th>
222      * <th>description</th>
223      * </tr>
224      * </thead>
225      * <tbody>
226      * <tr>
227      * <td>maxThreads</td>
228      * <td>256</td>
229      * <td>The max number of threads of HttpClient's Executor.  If not set, or set to the value of "-", then the
230      * Jetty server thread pool will be used.</td>
231      * </tr>
232      * <tr>
233      * <td>maxConnections</td>
234      * <td>32768</td>
235      * <td>The max number of connections per destination, see {@link HttpClient#setMaxConnectionsPerDestination(int)}</td>
236      * </tr>
237      * <tr>
238      * <td>idleTimeout</td>
239      * <td>30000</td>
240      * <td>The idle timeout in milliseconds, see {@link HttpClient#setIdleTimeout(long)}</td>
241      * </tr>
242      * <tr>
243      * <td>timeout</td>
244      * <td>60000</td>
245      * <td>The total timeout in milliseconds, see {@link Request#timeout(long, java.util.concurrent.TimeUnit)}</td>
246      * </tr>
247      * <tr>
248      * <td>requestBufferSize</td>
249      * <td>HttpClient's default</td>
250      * <td>The request buffer size, see {@link HttpClient#setRequestBufferSize(int)}</td>
251      * </tr>
252      * <tr>
253      * <td>responseBufferSize</td>
254      * <td>HttpClient's default</td>
255      * <td>The response buffer size, see {@link HttpClient#setResponseBufferSize(int)}</td>
256      * </tr>
257      * </tbody>
258      * </table>
259      *
260      * @return a {@link HttpClient} configured from the {@link #getServletConfig() servlet configuration}
261      * @throws ServletException if the {@link HttpClient} cannot be created
262      */
263     protected HttpClient createHttpClient() throws ServletException
264     {
265         ServletConfig config = getServletConfig();
266 
267         HttpClient client = newHttpClient();
268 
269         // Redirects must be proxied as is, not followed.
270         client.setFollowRedirects(false);
271 
272         // Must not store cookies, otherwise cookies of different clients will mix.
273         client.setCookieStore(new HttpCookieStore.Empty());
274 
275         Executor executor;
276         String value = config.getInitParameter("maxThreads");
277         if (value == null || "-".equals(value))
278         {
279             executor = (Executor)getServletContext().getAttribute("org.eclipse.jetty.server.Executor");
280             if (executor==null)
281                 throw new IllegalStateException("No server executor for proxy");
282         }
283         else
284         {
285             QueuedThreadPool qtp= new QueuedThreadPool(Integer.parseInt(value));
286             String servletName = config.getServletName();
287             int dot = servletName.lastIndexOf('.');
288             if (dot >= 0)
289                 servletName = servletName.substring(dot + 1);
290             qtp.setName(servletName);
291             executor=qtp;
292         }
293 
294         client.setExecutor(executor);
295 
296         value = config.getInitParameter("maxConnections");
297         if (value == null)
298             value = "256";
299         client.setMaxConnectionsPerDestination(Integer.parseInt(value));
300 
301         value = config.getInitParameter("idleTimeout");
302         if (value == null)
303             value = "30000";
304         client.setIdleTimeout(Long.parseLong(value));
305 
306         value = config.getInitParameter("timeout");
307         if (value == null)
308             value = "60000";
309         _timeout = Long.parseLong(value);
310 
311         value = config.getInitParameter("requestBufferSize");
312         if (value != null)
313             client.setRequestBufferSize(Integer.parseInt(value));
314 
315         value = config.getInitParameter("responseBufferSize");
316         if (value != null)
317             client.setResponseBufferSize(Integer.parseInt(value));
318 
319         try
320         {
321             client.start();
322 
323             // Content must not be decoded, otherwise the client gets confused.
324             client.getContentDecoderFactories().clear();
325 
326             // No protocol handlers, pass everything to the client.
327             client.getProtocolHandlers().clear();
328 
329             return client;
330         }
331         catch (Exception x)
332         {
333             throw new ServletException(x);
334         }
335     }
336 
337     /**
338      * @return a new HttpClient instance
339      */
340     protected HttpClient newHttpClient()
341     {
342         return new HttpClient();
343     }
344 
345     protected HttpClient getHttpClient()
346     {
347         return _client;
348     }
349 
350     private Set<String> parseList(String list)
351     {
352         Set<String> result = new HashSet<>();
353         String[] hosts = list.split(",");
354         for (String host : hosts)
355         {
356             host = host.trim();
357             if (host.length() == 0)
358                 continue;
359             result.add(host);
360         }
361         return result;
362     }
363 
364     /**
365      * Checks the given {@code host} and {@code port} against whitelist and blacklist.
366      *
367      * @param host the host to check
368      * @param port the port to check
369      * @return true if it is allowed to be proxy to the given host and port
370      */
371     public boolean validateDestination(String host, int port)
372     {
373         String hostPort = host + ":" + port;
374         if (!_whiteList.isEmpty())
375         {
376             if (!_whiteList.contains(hostPort))
377             {
378                 if (_log.isDebugEnabled())
379                     _log.debug("Host {}:{} not whitelisted", host, port);
380                 return false;
381             }
382         }
383         if (!_blackList.isEmpty())
384         {
385             if (_blackList.contains(hostPort))
386             {
387                 if (_log.isDebugEnabled())
388                     _log.debug("Host {}:{} blacklisted", host, port);
389                 return false;
390             }
391         }
392         return true;
393     }
394 
395     protected String rewriteTarget(HttpServletRequest clientRequest)
396     {
397         if (!validateDestination(clientRequest.getServerName(), clientRequest.getServerPort()))
398             return null;
399 
400         StringBuffer target = clientRequest.getRequestURL();
401         String query = clientRequest.getQueryString();
402         if (query != null)
403             target.append("?").append(query);
404         return target.toString();
405     }
406 
407     /**
408      * <p>Callback method invoked when the URI rewrite performed
409      * in {@link #rewriteTarget(HttpServletRequest)} returns null
410      * indicating that no rewrite can be performed.</p>
411      * <p>It is possible to use blocking API in this method,
412      * like {@link HttpServletResponse#sendError(int)}.</p>
413      *
414      * @param clientRequest the client request
415      * @param clientResponse the client response
416      */
417     protected void onProxyRewriteFailed(HttpServletRequest clientRequest, HttpServletResponse clientResponse)
418     {
419         clientResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
420     }
421 
422     protected boolean hasContent(HttpServletRequest clientRequest)
423     {
424         return clientRequest.getContentLength() > 0 ||
425                 clientRequest.getContentType() != null ||
426                 clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
427     }
428 
429     protected void copyRequestHeaders(HttpServletRequest clientRequest, Request proxyRequest)
430     {
431         // First clear possibly existing headers, as we are going to copy those from the client request.
432         proxyRequest.getHeaders().clear();
433 
434         Set<String> headersToRemove = findConnectionHeaders(clientRequest);
435 
436         for (Enumeration<String> headerNames = clientRequest.getHeaderNames(); headerNames.hasMoreElements();)
437         {
438             String headerName = headerNames.nextElement();
439             String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
440 
441             if (HttpHeader.HOST.is(headerName) && !_preserveHost)
442                 continue;
443 
444             // Remove hop-by-hop headers.
445             if (HOP_HEADERS.contains(lowerHeaderName))
446                 continue;
447             if (headersToRemove != null && headersToRemove.contains(lowerHeaderName))
448                 continue;
449 
450             for (Enumeration<String> headerValues = clientRequest.getHeaders(headerName); headerValues.hasMoreElements();)
451             {
452                 String headerValue = headerValues.nextElement();
453                 if (headerValue != null)
454                     proxyRequest.header(headerName, headerValue);
455             }
456         }
457 
458         // Force the Host header if configured
459         if (_hostHeader != null)
460             proxyRequest.header(HttpHeader.HOST, _hostHeader);
461     }
462 
463     protected Set<String> findConnectionHeaders(HttpServletRequest clientRequest)
464     {
465         // Any header listed by the Connection header must be removed:
466         // http://tools.ietf.org/html/rfc7230#section-6.1.
467         Set<String> hopHeaders = null;
468         Enumeration<String> connectionHeaders = clientRequest.getHeaders(HttpHeader.CONNECTION.asString());
469         while (connectionHeaders.hasMoreElements())
470         {
471             String value = connectionHeaders.nextElement();
472             String[] values = value.split(",");
473             for (String name : values)
474             {
475                 name = name.trim().toLowerCase(Locale.ENGLISH);
476                 if (hopHeaders == null)
477                     hopHeaders = new HashSet<>();
478                 hopHeaders.add(name);
479             }
480         }
481         return hopHeaders;
482     }
483 
484     protected void addProxyHeaders(HttpServletRequest clientRequest, Request proxyRequest)
485     {
486         addViaHeader(proxyRequest);
487         addXForwardedHeaders(clientRequest, proxyRequest);
488     }
489 
490     protected void addViaHeader(Request proxyRequest)
491     {
492         proxyRequest.header(HttpHeader.VIA, "http/1.1 " + getViaHost());
493     }
494 
495     protected void addXForwardedHeaders(HttpServletRequest clientRequest, Request proxyRequest)
496     {
497         proxyRequest.header(HttpHeader.X_FORWARDED_FOR, clientRequest.getRemoteAddr());
498         proxyRequest.header(HttpHeader.X_FORWARDED_PROTO, clientRequest.getScheme());
499         proxyRequest.header(HttpHeader.X_FORWARDED_HOST, clientRequest.getHeader(HttpHeader.HOST.asString()));
500         proxyRequest.header(HttpHeader.X_FORWARDED_SERVER, clientRequest.getLocalName());
501     }
502 
503     protected void sendProxyRequest(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest)
504     {
505         if (_log.isDebugEnabled())
506         {
507             StringBuilder builder = new StringBuilder(clientRequest.getMethod());
508             builder.append(" ").append(clientRequest.getRequestURI());
509             String query = clientRequest.getQueryString();
510             if (query != null)
511                 builder.append("?").append(query);
512             builder.append(" ").append(clientRequest.getProtocol()).append(System.lineSeparator());
513             for (Enumeration<String> headerNames = clientRequest.getHeaderNames(); headerNames.hasMoreElements();)
514             {
515                 String headerName = headerNames.nextElement();
516                 builder.append(headerName).append(": ");
517                 for (Enumeration<String> headerValues = clientRequest.getHeaders(headerName); headerValues.hasMoreElements();)
518                 {
519                     String headerValue = headerValues.nextElement();
520                     if (headerValue != null)
521                         builder.append(headerValue);
522                     if (headerValues.hasMoreElements())
523                         builder.append(",");
524                 }
525                 builder.append(System.lineSeparator());
526             }
527             builder.append(System.lineSeparator());
528 
529             _log.debug("{} proxying to upstream:{}{}{}{}",
530                     getRequestId(clientRequest),
531                     System.lineSeparator(),
532                     builder,
533                     proxyRequest,
534                     System.lineSeparator(),
535                     proxyRequest.getHeaders().toString().trim());
536         }
537 
538         proxyRequest.send(newProxyResponseListener(clientRequest, proxyResponse));
539     }
540 
541     protected abstract Response.CompleteListener newProxyResponseListener(HttpServletRequest clientRequest, HttpServletResponse proxyResponse);
542 
543     protected void onClientRequestFailure(HttpServletRequest clientRequest, Request proxyRequest, HttpServletResponse proxyResponse, Throwable failure)
544     {
545         boolean aborted = proxyRequest.abort(failure);
546         if (!aborted)
547         {
548             int status = failure instanceof TimeoutException ?
549                     HttpStatus.REQUEST_TIMEOUT_408 :
550                     HttpStatus.INTERNAL_SERVER_ERROR_500;
551             proxyResponse.setStatus(status);
552             clientRequest.getAsyncContext().complete();
553         }
554     }
555 
556     protected void onServerResponseHeaders(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Response serverResponse)
557     {
558         for (HttpField field : serverResponse.getHeaders())
559         {
560             String headerName = field.getName();
561             String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
562             if (HOP_HEADERS.contains(lowerHeaderName))
563                 continue;
564 
565             String newHeaderValue = filterServerResponseHeader(clientRequest, serverResponse, headerName, field.getValue());
566             if (newHeaderValue == null || newHeaderValue.trim().length() == 0)
567                 continue;
568 
569             proxyResponse.addHeader(headerName, newHeaderValue);
570         }
571 
572         if (_log.isDebugEnabled())
573         {
574             StringBuilder builder = new StringBuilder(System.lineSeparator());
575             builder.append(clientRequest.getProtocol()).append(" ").append(proxyResponse.getStatus())
576                     .append(" ").append(serverResponse.getReason()).append(System.lineSeparator());
577             for (String headerName : proxyResponse.getHeaderNames())
578             {
579                 builder.append(headerName).append(": ");
580                 for (Iterator<String> headerValues = proxyResponse.getHeaders(headerName).iterator(); headerValues.hasNext(); )
581                 {
582                     String headerValue = headerValues.next();
583                     if (headerValue != null)
584                         builder.append(headerValue);
585                     if (headerValues.hasNext())
586                         builder.append(",");
587                 }
588                 builder.append(System.lineSeparator());
589             }
590             _log.debug("{} proxying to downstream:{}{}{}{}{}",
591                     getRequestId(clientRequest),
592                     System.lineSeparator(),
593                     serverResponse,
594                     System.lineSeparator(),
595                     serverResponse.getHeaders().toString().trim(),
596                     System.lineSeparator(),
597                     builder);
598         }
599     }
600 
601     protected String filterServerResponseHeader(HttpServletRequest clientRequest, Response serverResponse, String headerName, String headerValue)
602     {
603         return headerValue;
604     }
605 
606     protected void onProxyResponseSuccess(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Response serverResponse)
607     {
608         if (_log.isDebugEnabled())
609             _log.debug("{} proxying successful", getRequestId(clientRequest));
610 
611         AsyncContext asyncContext = clientRequest.getAsyncContext();
612         asyncContext.complete();
613     }
614 
615     protected void onProxyResponseFailure(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Response serverResponse, Throwable failure)
616     {
617         if (_log.isDebugEnabled())
618             _log.debug(getRequestId(clientRequest) + " proxying failed", failure);
619 
620         if (proxyResponse.isCommitted())
621         {
622             try
623             {
624                 // Use Jetty specific behavior to close connection.
625                 proxyResponse.sendError(-1);
626                 AsyncContext asyncContext = clientRequest.getAsyncContext();
627                 asyncContext.complete();
628             }
629             catch (IOException x)
630             {
631                 if (_log.isDebugEnabled())
632                     _log.debug(getRequestId(clientRequest) + " could not close the connection", failure);
633             }
634         }
635         else
636         {
637             proxyResponse.resetBuffer();
638             if (failure instanceof TimeoutException)
639                 proxyResponse.setStatus(HttpServletResponse.SC_GATEWAY_TIMEOUT);
640             else
641                 proxyResponse.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
642             proxyResponse.setHeader(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString());
643             AsyncContext asyncContext = clientRequest.getAsyncContext();
644             asyncContext.complete();
645         }
646     }
647 
648     protected int getRequestId(HttpServletRequest clientRequest)
649     {
650         return System.identityHashCode(clientRequest);
651     }
652 
653     /**
654      * <p>Utility class that implement transparent proxy functionalities.</p>
655      * <p>Configuration parameters:</p>
656      * <ul>
657      * <li>{@code proxyTo} - a mandatory URI like http://host:80/context to which the request is proxied.</li>
658      * <li>{@code prefix} - an optional URI prefix that is stripped from the start of the forwarded URI.</li>
659      * </ul>
660      * <p>For example, if a request is received at "/foo/bar", the {@code proxyTo} parameter is
661      * "http://host:80/context" and the {@code prefix} parameter is "/foo", then the request would
662      * be proxied to "http://host:80/context/bar".
663      */
664     protected static class TransparentDelegate
665     {
666         private final ProxyServlet proxyServlet;
667         private String _proxyTo;
668         private String _prefix;
669 
670         protected TransparentDelegate(ProxyServlet proxyServlet)
671         {
672             this.proxyServlet = proxyServlet;
673         }
674 
675         protected void init(ServletConfig config) throws ServletException
676         {
677             _proxyTo = config.getInitParameter("proxyTo");
678             if (_proxyTo == null)
679                 throw new UnavailableException("Init parameter 'proxyTo' is required.");
680 
681             String prefix = config.getInitParameter("prefix");
682             if (prefix != null)
683             {
684                 if (!prefix.startsWith("/"))
685                     throw new UnavailableException("Init parameter 'prefix' must start with a '/'.");
686                 _prefix = prefix;
687             }
688 
689             // Adjust prefix value to account for context path
690             String contextPath = config.getServletContext().getContextPath();
691             _prefix = _prefix == null ? contextPath : (contextPath + _prefix);
692 
693             if (proxyServlet._log.isDebugEnabled())
694                 proxyServlet._log.debug(config.getServletName() + " @ " + _prefix + " to " + _proxyTo);
695         }
696 
697         protected String rewriteTarget(HttpServletRequest request)
698         {
699             String path = request.getRequestURI();
700             if (!path.startsWith(_prefix))
701                 return null;
702 
703             StringBuilder uri = new StringBuilder(_proxyTo);
704             if (_proxyTo.endsWith("/"))
705                 uri.setLength(uri.length() - 1);
706             String rest = path.substring(_prefix.length());
707             if (!rest.isEmpty())
708             {
709                 if (!rest.startsWith("/"))
710                     uri.append("/");
711                 uri.append(rest);
712             }
713 
714             String query = request.getQueryString();
715             if (query != null)
716             {
717                 // Is there at least one path segment ?
718                 String separator = "://";
719                 if (uri.indexOf("/", uri.indexOf(separator) + separator.length()) < 0)
720                     uri.append("/");
721                 uri.append("?").append(query);
722             }
723             URI rewrittenURI = URI.create(uri.toString()).normalize();
724 
725             if (!proxyServlet.validateDestination(rewrittenURI.getHost(), rewrittenURI.getPort()))
726                 return null;
727 
728             return rewrittenURI.toString();
729         }
730     }
731 }