View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 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      * <caption>Init Parameters</caption>
219      * <thead>
220      * <tr>
221      * <th>init-param</th>
222      * <th>default</th>
223      * <th>description</th>
224      * </tr>
225      * </thead>
226      * <tbody>
227      * <tr>
228      * <td>maxThreads</td>
229      * <td>256</td>
230      * <td>The max number of threads of HttpClient's Executor.  If not set, or set to the value of "-", then the
231      * Jetty server thread pool will be used.</td>
232      * </tr>
233      * <tr>
234      * <td>maxConnections</td>
235      * <td>32768</td>
236      * <td>The max number of connections per destination, see {@link HttpClient#setMaxConnectionsPerDestination(int)}</td>
237      * </tr>
238      * <tr>
239      * <td>idleTimeout</td>
240      * <td>30000</td>
241      * <td>The idle timeout in milliseconds, see {@link HttpClient#setIdleTimeout(long)}</td>
242      * </tr>
243      * <tr>
244      * <td>timeout</td>
245      * <td>60000</td>
246      * <td>The total timeout in milliseconds, see {@link Request#timeout(long, java.util.concurrent.TimeUnit)}</td>
247      * </tr>
248      * <tr>
249      * <td>requestBufferSize</td>
250      * <td>HttpClient's default</td>
251      * <td>The request buffer size, see {@link HttpClient#setRequestBufferSize(int)}</td>
252      * </tr>
253      * <tr>
254      * <td>responseBufferSize</td>
255      * <td>HttpClient's default</td>
256      * <td>The response buffer size, see {@link HttpClient#setResponseBufferSize(int)}</td>
257      * </tr>
258      * </tbody>
259      * </table>
260      *
261      * @return a {@link HttpClient} configured from the {@link #getServletConfig() servlet configuration}
262      * @throws ServletException if the {@link HttpClient} cannot be created
263      */
264     protected HttpClient createHttpClient() throws ServletException
265     {
266         ServletConfig config = getServletConfig();
267 
268         HttpClient client = newHttpClient();
269 
270         // Redirects must be proxied as is, not followed.
271         client.setFollowRedirects(false);
272 
273         // Must not store cookies, otherwise cookies of different clients will mix.
274         client.setCookieStore(new HttpCookieStore.Empty());
275 
276         Executor executor;
277         String value = config.getInitParameter("maxThreads");
278         if (value == null || "-".equals(value))
279         {
280             executor = (Executor)getServletContext().getAttribute("org.eclipse.jetty.server.Executor");
281             if (executor==null)
282                 throw new IllegalStateException("No server executor for proxy");
283         }
284         else
285         {
286             QueuedThreadPool qtp= new QueuedThreadPool(Integer.parseInt(value));
287             String servletName = config.getServletName();
288             int dot = servletName.lastIndexOf('.');
289             if (dot >= 0)
290                 servletName = servletName.substring(dot + 1);
291             qtp.setName(servletName);
292             executor=qtp;
293         }
294 
295         client.setExecutor(executor);
296 
297         value = config.getInitParameter("maxConnections");
298         if (value == null)
299             value = "256";
300         client.setMaxConnectionsPerDestination(Integer.parseInt(value));
301 
302         value = config.getInitParameter("idleTimeout");
303         if (value == null)
304             value = "30000";
305         client.setIdleTimeout(Long.parseLong(value));
306 
307         value = config.getInitParameter("timeout");
308         if (value == null)
309             value = "60000";
310         _timeout = Long.parseLong(value);
311 
312         value = config.getInitParameter("requestBufferSize");
313         if (value != null)
314             client.setRequestBufferSize(Integer.parseInt(value));
315 
316         value = config.getInitParameter("responseBufferSize");
317         if (value != null)
318             client.setResponseBufferSize(Integer.parseInt(value));
319 
320         try
321         {
322             client.start();
323 
324             // Content must not be decoded, otherwise the client gets confused.
325             client.getContentDecoderFactories().clear();
326 
327             // No protocol handlers, pass everything to the client.
328             client.getProtocolHandlers().clear();
329 
330             return client;
331         }
332         catch (Exception x)
333         {
334             throw new ServletException(x);
335         }
336     }
337 
338     /**
339      * @return a new HttpClient instance
340      */
341     protected HttpClient newHttpClient()
342     {
343         return new HttpClient();
344     }
345 
346     protected HttpClient getHttpClient()
347     {
348         return _client;
349     }
350 
351     private Set<String> parseList(String list)
352     {
353         Set<String> result = new HashSet<>();
354         String[] hosts = list.split(",");
355         for (String host : hosts)
356         {
357             host = host.trim();
358             if (host.length() == 0)
359                 continue;
360             result.add(host);
361         }
362         return result;
363     }
364 
365     /**
366      * Checks the given {@code host} and {@code port} against whitelist and blacklist.
367      *
368      * @param host the host to check
369      * @param port the port to check
370      * @return true if it is allowed to be proxy to the given host and port
371      */
372     public boolean validateDestination(String host, int port)
373     {
374         String hostPort = host + ":" + port;
375         if (!_whiteList.isEmpty())
376         {
377             if (!_whiteList.contains(hostPort))
378             {
379                 if (_log.isDebugEnabled())
380                     _log.debug("Host {}:{} not whitelisted", host, port);
381                 return false;
382             }
383         }
384         if (!_blackList.isEmpty())
385         {
386             if (_blackList.contains(hostPort))
387             {
388                 if (_log.isDebugEnabled())
389                     _log.debug("Host {}:{} blacklisted", host, port);
390                 return false;
391             }
392         }
393         return true;
394     }
395 
396     protected String rewriteTarget(HttpServletRequest clientRequest)
397     {
398         if (!validateDestination(clientRequest.getServerName(), clientRequest.getServerPort()))
399             return null;
400 
401         StringBuffer target = clientRequest.getRequestURL();
402         String query = clientRequest.getQueryString();
403         if (query != null)
404             target.append("?").append(query);
405         return target.toString();
406     }
407 
408     /**
409      * <p>Callback method invoked when the URI rewrite performed
410      * in {@link #rewriteTarget(HttpServletRequest)} returns null
411      * indicating that no rewrite can be performed.</p>
412      * <p>It is possible to use blocking API in this method,
413      * like {@link HttpServletResponse#sendError(int)}.</p>
414      *
415      * @param clientRequest the client request
416      * @param proxyResponse the client response
417      */
418     protected void onProxyRewriteFailed(HttpServletRequest clientRequest, HttpServletResponse proxyResponse)
419     {
420         sendProxyResponseError(clientRequest, proxyResponse, HttpStatus.FORBIDDEN_403);
421     }
422 
423     protected boolean hasContent(HttpServletRequest clientRequest)
424     {
425         return clientRequest.getContentLength() > 0 ||
426                 clientRequest.getContentType() != null ||
427                 clientRequest.getHeader(HttpHeader.TRANSFER_ENCODING.asString()) != null;
428     }
429 
430     protected void copyRequestHeaders(HttpServletRequest clientRequest, Request proxyRequest)
431     {
432         // First clear possibly existing headers, as we are going to copy those from the client request.
433         proxyRequest.getHeaders().clear();
434 
435         Set<String> headersToRemove = findConnectionHeaders(clientRequest);
436 
437         for (Enumeration<String> headerNames = clientRequest.getHeaderNames(); headerNames.hasMoreElements();)
438         {
439             String headerName = headerNames.nextElement();
440             String lowerHeaderName = headerName.toLowerCase(Locale.ENGLISH);
441 
442             if (HttpHeader.HOST.is(headerName) && !_preserveHost)
443                 continue;
444 
445             // Remove hop-by-hop headers.
446             if (HOP_HEADERS.contains(lowerHeaderName))
447                 continue;
448             if (headersToRemove != null && headersToRemove.contains(lowerHeaderName))
449                 continue;
450 
451             for (Enumeration<String> headerValues = clientRequest.getHeaders(headerName); headerValues.hasMoreElements();)
452             {
453                 String headerValue = headerValues.nextElement();
454                 if (headerValue != null)
455                     proxyRequest.header(headerName, headerValue);
456             }
457         }
458 
459         // Force the Host header if configured
460         if (_hostHeader != null)
461             proxyRequest.header(HttpHeader.HOST, _hostHeader);
462     }
463 
464     protected Set<String> findConnectionHeaders(HttpServletRequest clientRequest)
465     {
466         // Any header listed by the Connection header must be removed:
467         // http://tools.ietf.org/html/rfc7230#section-6.1.
468         Set<String> hopHeaders = null;
469         Enumeration<String> connectionHeaders = clientRequest.getHeaders(HttpHeader.CONNECTION.asString());
470         while (connectionHeaders.hasMoreElements())
471         {
472             String value = connectionHeaders.nextElement();
473             String[] values = value.split(",");
474             for (String name : values)
475             {
476                 name = name.trim().toLowerCase(Locale.ENGLISH);
477                 if (hopHeaders == null)
478                     hopHeaders = new HashSet<>();
479                 hopHeaders.add(name);
480             }
481         }
482         return hopHeaders;
483     }
484 
485     protected void addProxyHeaders(HttpServletRequest clientRequest, Request proxyRequest)
486     {
487         addViaHeader(proxyRequest);
488         addXForwardedHeaders(clientRequest, proxyRequest);
489     }
490 
491     protected void addViaHeader(Request proxyRequest)
492     {
493         proxyRequest.header(HttpHeader.VIA, "http/1.1 " + getViaHost());
494     }
495 
496     protected void addXForwardedHeaders(HttpServletRequest clientRequest, Request proxyRequest)
497     {
498         proxyRequest.header(HttpHeader.X_FORWARDED_FOR, clientRequest.getRemoteAddr());
499         proxyRequest.header(HttpHeader.X_FORWARDED_PROTO, clientRequest.getScheme());
500         proxyRequest.header(HttpHeader.X_FORWARDED_HOST, clientRequest.getHeader(HttpHeader.HOST.asString()));
501         proxyRequest.header(HttpHeader.X_FORWARDED_SERVER, clientRequest.getLocalName());
502     }
503 
504     protected void sendProxyRequest(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest)
505     {
506         if (_log.isDebugEnabled())
507         {
508             StringBuilder builder = new StringBuilder(clientRequest.getMethod());
509             builder.append(" ").append(clientRequest.getRequestURI());
510             String query = clientRequest.getQueryString();
511             if (query != null)
512                 builder.append("?").append(query);
513             builder.append(" ").append(clientRequest.getProtocol()).append(System.lineSeparator());
514             for (Enumeration<String> headerNames = clientRequest.getHeaderNames(); headerNames.hasMoreElements();)
515             {
516                 String headerName = headerNames.nextElement();
517                 builder.append(headerName).append(": ");
518                 for (Enumeration<String> headerValues = clientRequest.getHeaders(headerName); headerValues.hasMoreElements();)
519                 {
520                     String headerValue = headerValues.nextElement();
521                     if (headerValue != null)
522                         builder.append(headerValue);
523                     if (headerValues.hasMoreElements())
524                         builder.append(",");
525                 }
526                 builder.append(System.lineSeparator());
527             }
528             builder.append(System.lineSeparator());
529 
530             _log.debug("{} proxying to upstream:{}{}{}{}",
531                     getRequestId(clientRequest),
532                     System.lineSeparator(),
533                     builder,
534                     proxyRequest,
535                     System.lineSeparator(),
536                     proxyRequest.getHeaders().toString().trim());
537         }
538 
539         proxyRequest.send(newProxyResponseListener(clientRequest, proxyResponse));
540     }
541 
542     protected abstract Response.CompleteListener newProxyResponseListener(HttpServletRequest clientRequest, HttpServletResponse proxyResponse);
543 
544     protected void onClientRequestFailure(HttpServletRequest clientRequest, Request proxyRequest, HttpServletResponse proxyResponse, Throwable failure)
545     {
546         boolean aborted = proxyRequest.abort(failure);
547         if (!aborted)
548         {
549             int status = failure instanceof TimeoutException ?
550                     HttpStatus.REQUEST_TIMEOUT_408 :
551                     HttpStatus.INTERNAL_SERVER_ERROR_500;
552             sendProxyResponseError(clientRequest, proxyResponse, status);
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             int status = failure instanceof TimeoutException ?
639                     HttpStatus.GATEWAY_TIMEOUT_504 :
640                     HttpStatus.BAD_GATEWAY_502;
641             sendProxyResponseError(clientRequest, proxyResponse, status);
642         }
643     }
644 
645     protected int getRequestId(HttpServletRequest clientRequest)
646     {
647         return System.identityHashCode(clientRequest);
648     }
649 
650     protected void sendProxyResponseError(HttpServletRequest clientRequest, HttpServletResponse proxyResponse, int status)
651     {
652         proxyResponse.setStatus(status);
653         proxyResponse.setHeader(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString());
654         if (clientRequest.isAsyncStarted())
655             clientRequest.getAsyncContext().complete();
656     }
657 
658     /**
659      * <p>Utility class that implement transparent proxy functionalities.</p>
660      * <p>Configuration parameters:</p>
661      * <ul>
662      * <li>{@code proxyTo} - a mandatory URI like http://host:80/context to which the request is proxied.</li>
663      * <li>{@code prefix} - an optional URI prefix that is stripped from the start of the forwarded URI.</li>
664      * </ul>
665      * <p>For example, if a request is received at "/foo/bar", the {@code proxyTo} parameter is
666      * "http://host:80/context" and the {@code prefix} parameter is "/foo", then the request would
667      * be proxied to "http://host:80/context/bar".
668      */
669     protected static class TransparentDelegate
670     {
671         private final ProxyServlet proxyServlet;
672         private String _proxyTo;
673         private String _prefix;
674 
675         protected TransparentDelegate(ProxyServlet proxyServlet)
676         {
677             this.proxyServlet = proxyServlet;
678         }
679 
680         protected void init(ServletConfig config) throws ServletException
681         {
682             _proxyTo = config.getInitParameter("proxyTo");
683             if (_proxyTo == null)
684                 throw new UnavailableException("Init parameter 'proxyTo' is required.");
685 
686             String prefix = config.getInitParameter("prefix");
687             if (prefix != null)
688             {
689                 if (!prefix.startsWith("/"))
690                     throw new UnavailableException("Init parameter 'prefix' must start with a '/'.");
691                 _prefix = prefix;
692             }
693 
694             // Adjust prefix value to account for context path
695             String contextPath = config.getServletContext().getContextPath();
696             _prefix = _prefix == null ? contextPath : (contextPath + _prefix);
697 
698             if (proxyServlet._log.isDebugEnabled())
699                 proxyServlet._log.debug(config.getServletName() + " @ " + _prefix + " to " + _proxyTo);
700         }
701 
702         protected String rewriteTarget(HttpServletRequest request)
703         {
704             String path = request.getRequestURI();
705             if (!path.startsWith(_prefix))
706                 return null;
707 
708             StringBuilder uri = new StringBuilder(_proxyTo);
709             if (_proxyTo.endsWith("/"))
710                 uri.setLength(uri.length() - 1);
711             String rest = path.substring(_prefix.length());
712             if (!rest.isEmpty())
713             {
714                 if (!rest.startsWith("/"))
715                     uri.append("/");
716                 uri.append(rest);
717             }
718 
719             String query = request.getQueryString();
720             if (query != null)
721             {
722                 // Is there at least one path segment ?
723                 String separator = "://";
724                 if (uri.indexOf("/", uri.indexOf(separator) + separator.length()) < 0)
725                     uri.append("/");
726                 uri.append("?").append(query);
727             }
728             URI rewrittenURI = URI.create(uri.toString()).normalize();
729 
730             if (!proxyServlet.validateDestination(rewrittenURI.getHost(), rewrittenURI.getPort()))
731                 return null;
732 
733             return rewrittenURI.toString();
734         }
735     }
736 }