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 FASTCGI_HTTPS_INIT_PARAM = "fastCGI.HTTPS";
75  
76      private static final String REMOTE_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remoteAddr";
77      private static final String REMOTE_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".remotePort";
78      private static final String SERVER_NAME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverName";
79      private static final String SERVER_ADDR_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverAddr";
80      private static final String SERVER_PORT_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".serverPort";
81      private static final String SCHEME_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".scheme";
82      private static final String REQUEST_URI_ATTRIBUTE = FastCGIProxyServlet.class.getName() + ".requestURI";
83  
84      private Pattern scriptPattern;
85      private String originalURIAttribute;
86      private boolean fcgiHTTPS;
87  
88      @Override
89      public void init() throws ServletException
90      {
91          super.init();
92  
93          String value = getInitParameter(SCRIPT_PATTERN_INIT_PARAM);
94          if (value == null)
95              value = "(.+?\\.php)";
96          scriptPattern = Pattern.compile(value);
97  
98          originalURIAttribute = getInitParameter(ORIGINAL_URI_ATTRIBUTE_INIT_PARAM);
99  
100         fcgiHTTPS = Boolean.parseBoolean(getInitParameter(FASTCGI_HTTPS_INIT_PARAM));
101     }
102 
103     @Override
104     protected HttpClient newHttpClient()
105     {
106         ServletConfig config = getServletConfig();
107         String scriptRoot = config.getInitParameter(SCRIPT_ROOT_INIT_PARAM);
108         if (scriptRoot == null)
109             throw new IllegalArgumentException("Mandatory parameter '" + SCRIPT_ROOT_INIT_PARAM + "' not configured");
110         return new HttpClient(new ProxyHttpClientTransportOverFCGI(scriptRoot), null);
111     }
112 
113     @Override
114     protected void sendProxyRequest(HttpServletRequest request, HttpServletResponse proxyResponse, Request proxyRequest)
115     {
116         proxyRequest.attribute(REMOTE_ADDR_ATTRIBUTE, request.getRemoteAddr());
117         proxyRequest.attribute(REMOTE_PORT_ATTRIBUTE, String.valueOf(request.getRemotePort()));
118         proxyRequest.attribute(SERVER_NAME_ATTRIBUTE, request.getServerName());
119         proxyRequest.attribute(SERVER_ADDR_ATTRIBUTE, request.getLocalAddr());
120         proxyRequest.attribute(SERVER_PORT_ATTRIBUTE, String.valueOf(request.getLocalPort()));
121         proxyRequest.attribute(SCHEME_ATTRIBUTE, request.getScheme());
122 
123         // Has the original URI been rewritten ?
124         String originalURI = null;
125         if (originalURIAttribute != null)
126             originalURI = (String)request.getAttribute(originalURIAttribute);
127 
128         if (originalURI == null)
129         {
130             // If we are forwarded or included, retain the original request URI.
131             String originalPath = (String)request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
132             String originalQuery = (String)request.getAttribute(RequestDispatcher.FORWARD_QUERY_STRING);
133             if (originalPath == null)
134             {
135                 originalPath = (String)request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI);
136                 originalQuery = (String)request.getAttribute(RequestDispatcher.INCLUDE_QUERY_STRING);
137             }
138             if (originalPath != null)
139             {
140                 originalURI = originalPath;
141                 if (originalQuery != null)
142                     originalURI += "?" + originalQuery;
143             }
144         }
145 
146         if (originalURI != null)
147             proxyRequest.attribute(REQUEST_URI_ATTRIBUTE, originalURI);
148 
149         // If the Host header is missing, add it.
150         if (!proxyRequest.getHeaders().containsKey(HttpHeader.HOST.asString()))
151         {
152             String host = request.getServerName();
153             int port = request.getServerPort();
154             if (!getHttpClient().isDefaultPort(request.getScheme(), port))
155                 host += ":" + port;
156             proxyRequest.header(HttpHeader.HOST, host);
157             proxyRequest.header(HttpHeader.X_FORWARDED_HOST, host);
158         }
159 
160         // PHP does not like multiple Cookie headers, coalesce into one.
161         List<String> cookies = proxyRequest.getHeaders().getValuesList(HttpHeader.COOKIE.asString());
162         if (cookies.size() > 1)
163         {
164             StringBuilder builder = new StringBuilder();
165             for (int i = 0; i < cookies.size(); ++i)
166             {
167                 if (i > 0)
168                     builder.append("; ");
169                 String cookie = cookies.get(i);
170                 builder.append(cookie);
171             }
172             proxyRequest.header(HttpHeader.COOKIE, null);
173             proxyRequest.header(HttpHeader.COOKIE, builder.toString());
174         }
175 
176         super.sendProxyRequest(request, proxyResponse, proxyRequest);
177     }
178 
179     protected void customizeFastCGIHeaders(Request proxyRequest, HttpFields fastCGIHeaders)
180     {
181         fastCGIHeaders.put(FCGI.Headers.REMOTE_ADDR, (String)proxyRequest.getAttributes().get(REMOTE_ADDR_ATTRIBUTE));
182         fastCGIHeaders.put(FCGI.Headers.REMOTE_PORT, (String)proxyRequest.getAttributes().get(REMOTE_PORT_ATTRIBUTE));
183         fastCGIHeaders.put(FCGI.Headers.SERVER_NAME, (String)proxyRequest.getAttributes().get(SERVER_NAME_ATTRIBUTE));
184         fastCGIHeaders.put(FCGI.Headers.SERVER_ADDR, (String)proxyRequest.getAttributes().get(SERVER_ADDR_ATTRIBUTE));
185         fastCGIHeaders.put(FCGI.Headers.SERVER_PORT, (String)proxyRequest.getAttributes().get(SERVER_PORT_ATTRIBUTE));
186 
187         if (fcgiHTTPS || HttpScheme.HTTPS.is((String)proxyRequest.getAttributes().get(SCHEME_ATTRIBUTE)))
188             fastCGIHeaders.put(FCGI.Headers.HTTPS, "on");
189 
190         URI proxyRequestURI = proxyRequest.getURI();
191         String rawPath = proxyRequestURI.getRawPath();
192         String rawQuery = proxyRequestURI.getRawQuery();
193 
194         String requestURI = (String)proxyRequest.getAttributes().get(REQUEST_URI_ATTRIBUTE);
195         if (requestURI == null)
196         {
197             requestURI = rawPath;
198             if (rawQuery != null)
199                 requestURI += "?" + rawQuery;
200         }
201         fastCGIHeaders.put(FCGI.Headers.REQUEST_URI, requestURI);
202 
203         String scriptName = rawPath;
204         Matcher matcher = scriptPattern.matcher(rawPath);
205         if (matcher.matches())
206         {
207             // Expect at least one group in the regular expression.
208             scriptName = matcher.group(1);
209 
210             // If there is a second group, map it to PATH_INFO.
211             if (matcher.groupCount() > 1)
212                 fastCGIHeaders.put(FCGI.Headers.PATH_INFO, matcher.group(2));
213         }
214         fastCGIHeaders.put(FCGI.Headers.SCRIPT_NAME, scriptName);
215 
216         String root = fastCGIHeaders.get(FCGI.Headers.DOCUMENT_ROOT);
217         fastCGIHeaders.put(FCGI.Headers.SCRIPT_FILENAME, root + scriptName);
218     }
219 
220     private class ProxyHttpClientTransportOverFCGI extends HttpClientTransportOverFCGI
221     {
222         public ProxyHttpClientTransportOverFCGI(String scriptRoot)
223         {
224             super(scriptRoot);
225         }
226 
227         @Override
228         protected void customize(Request request, HttpFields fastCGIHeaders)
229         {
230             super.customize(request, fastCGIHeaders);
231             customizeFastCGIHeaders(request, fastCGIHeaders);
232             if (_log.isDebugEnabled())
233             {
234                 TreeMap<String, String> fcgi = new TreeMap<>();
235                 for (HttpField field : fastCGIHeaders)
236                     fcgi.put(field.getName(), field.getValue());
237                 String eol = System.lineSeparator();
238                 _log.debug("FastCGI variables{}{}", eol, fcgi.entrySet().stream()
239                         .map(entry -> String.format("%s: %s", entry.getKey(), entry.getValue()))
240                         .collect(Collectors.joining(eol)));
241             }
242         }
243     }
244 }