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.fcgi.server.proxy;
20  
21  import java.net.URI;
22  import java.util.List;
23  import java.util.TreeMap;
24  import java.util.regex.Matcher;
25  import java.util.regex.Pattern;
26  import java.util.stream.Collectors;
27  
28  import javax.servlet.RequestDispatcher;
29  import javax.servlet.ServletConfig;
30  import javax.servlet.ServletException;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  
34  import org.eclipse.jetty.client.HttpClient;
35  import org.eclipse.jetty.client.api.Request;
36  import org.eclipse.jetty.fcgi.FCGI;
37  import org.eclipse.jetty.fcgi.client.http.HttpClientTransportOverFCGI;
38  import org.eclipse.jetty.http.HttpField;
39  import org.eclipse.jetty.http.HttpFields;
40  import org.eclipse.jetty.http.HttpHeader;
41  import org.eclipse.jetty.http.HttpScheme;
42  import org.eclipse.jetty.proxy.AsyncProxyServlet;
43  
44  /**
45   * Specific implementation of {@link org.eclipse.jetty.proxy.AsyncProxyServlet.Transparent} for FastCGI.
46   * <p>
47   * This servlet accepts a HTTP request and transforms it into a FastCGI request
48   * that is sent to the FastCGI server specified in the <code>proxyTo</code>
49   * init-param.
50   * <p>
51   * This servlet accepts two additional init-params:
52   * <ul>
53   *     <li><code>scriptRoot</code>, mandatory, that must be set to the directory where
54   *     the application that must be served via FastCGI is installed and corresponds to
55   *     the FastCGI DOCUMENT_ROOT parameter</li>
56   *     <li><code>scriptPattern</code>, optional, defaults to <code>(.+?\.php)</code>,
57   *     that specifies a regular expression with at least 1 and at most 2 groups that specify
58   *     respectively:
59   *     <ul>
60   *         <li>the FastCGI SCRIPT_NAME parameter</li>
61   *         <li>the FastCGI PATH_INFO parameter</li>
62   *     </ul></li>
63   *     <li><code>fastCGI.HTTPS</code>, optional, defaults to false, that specifies whether
64   *     to force the FastCGI <code>HTTPS</code> parameter to the value <code>on</code></li>
65   * </ul>
66   *
67   * @see TryFilesFilter
68   */
69  public class FastCGIProxyServlet extends AsyncProxyServlet.Transparent
70  {
71      public static final String SCRIPT_ROOT_INIT_PARAM = "scriptRoot";
72      public static final String SCRIPT_PATTERN_INIT_PARAM = "scriptPattern";
73      public static final String ORIGINAL_URI_ATTRIBUTE_INIT_PARAM = "originalURIAttribute";
74      public static final String ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM = "originalQueryAttribute";
75      public static final String FASTCGI_HTTPS_INIT_PARAM = "fastCGI.HTTPS";
76  
77      private static final String REMOTE_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remoteAddr";
78      private static final String REMOTE_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remotePort";
79      private static final String SERVER_NAME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverName";
80      private static final String SERVER_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverAddr";
81      private static final String SERVER_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverPort";
82      private static final String SCHEME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".scheme";
83      private static final String REQUEST_URI_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestURI";
84      private static final String REQUEST_QUERY_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestQuery";
85  
86      private Pattern scriptPattern;
87      private String originalURIAttribute;
88      private String originalQueryAttribute;
89      private boolean fcgiHTTPS;
90  
91      @Override
92      public void init() throws ServletException
93      {
94          super.init();
95  
96          String value = getInitParameter(SCRIPT_PATTERN_INIT_PARAM);
97          if (value == null)
98              value = "(.+?\\.php)";
99          scriptPattern = Pattern.compile(value);
100 
101         originalURIAttribute = getInitParameter(ORIGINAL_URI_ATTRIBUTE_INIT_PARAM);
102         originalQueryAttribute = getInitParameter(ORIGINAL_QUERY_ATTRIBUTE_INIT_PARAM);
103 
104         fcgiHTTPS = Boolean.parseBoolean(getInitParameter(FASTCGI_HTTPS_INIT_PARAM));
105     }
106 
107     @Override
108     protected HttpClient newHttpClient()
109     {
110         ServletConfig config = getServletConfig();
111         String scriptRoot = config.getInitParameter(SCRIPT_ROOT_INIT_PARAM);
112         if (scriptRoot == null)
113             throw new IllegalArgumentException("Mandatory parameter '" + SCRIPT_ROOT_INIT_PARAM + "' not configured");
114         return new HttpClient(new ProxyHttpClientTransportOverFCGI(scriptRoot), null);
115     }
116 
117     @Override
118     protected void sendProxyRequest(HttpServletRequest request, HttpServletResponse proxyResponse, Request proxyRequest)
119     {
120         proxyRequest.attribute(REMOTE_ADDR_ATTRIBUTE, request.getRemoteAddr());
121         proxyRequest.attribute(REMOTE_PORT_ATTRIBUTE, String.valueOf(request.getRemotePort()));
122         proxyRequest.attribute(SERVER_NAME_ATTRIBUTE, request.getServerName());
123         proxyRequest.attribute(SERVER_ADDR_ATTRIBUTE, request.getLocalAddr());
124         proxyRequest.attribute(SERVER_PORT_ATTRIBUTE, String.valueOf(request.getLocalPort()));
125         proxyRequest.attribute(SCHEME_ATTRIBUTE, request.getScheme());
126 
127         // Has the original URI been rewritten ?
128         String originalURI = null;
129         String originalQuery = null;
130         if (originalURIAttribute != null)
131             originalURI = (String)request.getAttribute(originalURIAttribute);
132         if (originalURI != null && originalQueryAttribute != null)
133         {
134             originalQuery = (String)request.getAttribute(originalQueryAttribute);
135             if (originalQuery != null)
136                 originalURI += "?" + originalQuery;
137         }
138 
139         if (originalURI == null)
140         {
141             // If we are forwarded or included, retain the original request URI.
142             String originalPath = (String)request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
143             originalQuery = (String)request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
144             if (originalPath == null)
145             {
146                 originalPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
147                 originalQuery = (String)request.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
148             }
149             if (originalPath != null)
150             {
151                 originalURI = originalPath;
152                 if (originalQuery != null)
153                     originalURI += "?" + originalQuery;
154             }
155         }
156 
157         if (originalURI != null)
158             proxyRequest.attribute(REQUEST_URI_ATTRIBUTE, originalURI);
159         if (originalQuery != null)
160             proxyRequest.attribute(REQUEST_QUERY_ATTRIBUTE, originalQuery);
161 
162         // If the Host header is missing, add it.
163         if (!proxyRequest.getHeaders().containsKey(HttpHeader.HOST.asString()))
164         {
165             String host = request.getServerName();
166             int port = request.getServerPort();
167             if (!getHttpClient().isDefaultPort(request.getScheme(), port))
168                 host += ":" + port;
169             proxyRequest.header(HttpHeader.HOST, host);
170             proxyRequest.header(HttpHeader.X_FORWARDED_HOST, host);
171         }
172 
173         // PHP does not like multiple Cookie headers, coalesce into one.
174         List<String> cookies = proxyRequest.getHeaders().getValuesList(HttpHeader.COOKIE);
175         if (cookies.size() > 1)
176         {
177             StringBuilder builder = new StringBuilder();
178             for (int i = 0; i < cookies.size(); ++i)
179             {
180                 if (i > 0)
181                     builder.append("; ");
182                 String cookie = cookies.get(i);
183                 builder.append(cookie);
184             }
185             proxyRequest.header(HttpHeader.COOKIE, null);
186             proxyRequest.header(HttpHeader.COOKIE, builder.toString());
187         }
188 
189         super.sendProxyRequest(request, proxyResponse, proxyRequest);
190     }
191 
192     protected void customizeFastCGIHeaders(Request proxyRequest, HttpFields fastCGIHeaders)
193     {
194         fastCGIHeaders.remove("HTTP_PROXY");
195 
196         fastCGIHeaders.put(FCGI.Headers.REMOTE_ADDR, (String)proxyRequest.getAttributes().get(REMOTE_ADDR_ATTRIBUTE));
197         fastCGIHeaders.put(FCGI.Headers.REMOTE_PORT, (String)proxyRequest.getAttributes().get(REMOTE_PORT_ATTRIBUTE));
198         fastCGIHeaders.put(FCGI.Headers.SERVER_NAME, (String)proxyRequest.getAttributes().get(SERVER_NAME_ATTRIBUTE));
199         fastCGIHeaders.put(FCGI.Headers.SERVER_ADDR, (String)proxyRequest.getAttributes().get(SERVER_ADDR_ATTRIBUTE));
200         fastCGIHeaders.put(FCGI.Headers.SERVER_PORT, (String)proxyRequest.getAttributes().get(SERVER_PORT_ATTRIBUTE));
201 
202         if (fcgiHTTPS || HttpScheme.HTTPS.is((String)proxyRequest.getAttributes().get(SCHEME_ATTRIBUTE)))
203             fastCGIHeaders.put(FCGI.Headers.HTTPS, "on");
204 
205         URI proxyRequestURI = proxyRequest.getURI();
206         String rawPath = proxyRequestURI == null ? proxyRequest.getPath() : proxyRequestURI.getRawPath();
207         String rawQuery = proxyRequestURI == null ? null : proxyRequestURI.getRawQuery();
208 
209         String requestURI = (String)proxyRequest.getAttributes().get(REQUEST_URI_ATTRIBUTE);
210         if (requestURI == null)
211         {
212             requestURI = rawPath;
213             if (rawQuery != null)
214                 requestURI += "?" + rawQuery;
215         }
216         fastCGIHeaders.put(FCGI.Headers.REQUEST_URI, requestURI);
217 
218         String requestQuery = (String)proxyRequest.getAttributes().get(REQUEST_QUERY_ATTRIBUTE);
219         if (requestQuery != null)
220             fastCGIHeaders.put(FCGI.Headers.QUERY_STRING, requestQuery);
221 
222         String scriptName = rawPath;
223         Matcher matcher = scriptPattern.matcher(rawPath);
224         if (matcher.matches())
225         {
226             // Expect at least one group in the regular expression.
227             scriptName = matcher.group(1);
228 
229             // If there is a second group, map it to PATH_INFO.
230             if (matcher.groupCount() > 1)
231                 fastCGIHeaders.put(FCGI.Headers.PATH_INFO, matcher.group(2));
232         }
233         fastCGIHeaders.put(FCGI.Headers.SCRIPT_NAME, scriptName);
234 
235         String root = fastCGIHeaders.get(FCGI.Headers.DOCUMENT_ROOT);
236         fastCGIHeaders.put(FCGI.Headers.SCRIPT_FILENAME, root + scriptName);
237     }
238 
239     private class ProxyHttpClientTransportOverFCGI extends HttpClientTransportOverFCGI
240     {
241         public ProxyHttpClientTransportOverFCGI(String scriptRoot)
242         {
243             super(scriptRoot);
244         }
245 
246         @Override
247         protected void customize(Request request, HttpFields fastCGIHeaders)
248         {
249             super.customize(request, fastCGIHeaders);
250             customizeFastCGIHeaders(request, fastCGIHeaders);
251             if (_log.isDebugEnabled())
252             {
253                 TreeMap<String, String> fcgi = new TreeMap<>();
254                 for (HttpField field : fastCGIHeaders)
255                     fcgi.put(field.getName(), field.getValue());
256                 String eol = System.lineSeparator();
257                 _log.debug("FastCGI variables{}{}", eol, fcgi.entrySet().stream()
258                         .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue()))
259                         .collect(Collectors.joining(eol)));
260             }
261         }
262     }
263 }