001/*
002 *  Copyright 2011 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.site;
017
018import java.io.ByteArrayInputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Enumeration;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.Vector;
029import java.util.regex.Pattern;
030
031import javax.servlet.http.HttpServletRequest;
032
033import org.apache.cocoon.environment.ObjectModelHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.environment.Session;
036import org.apache.cocoon.environment.http.HttpEnvironment;
037import org.apache.cocoon.servlet.multipart.Part;
038import org.apache.commons.io.IOUtils;
039import org.apache.commons.lang.StringUtils;
040import org.apache.http.Consts;
041import org.apache.http.Header;
042import org.apache.http.HttpEntity;
043import org.apache.http.NameValuePair;
044import org.apache.http.client.entity.UrlEncodedFormEntity;
045import org.apache.http.client.methods.HttpGet;
046import org.apache.http.client.methods.HttpPost;
047import org.apache.http.client.methods.HttpUriRequest;
048import org.apache.http.entity.ContentType;
049import org.apache.http.entity.InputStreamEntity;
050import org.apache.http.entity.mime.HttpMultipartMode;
051import org.apache.http.entity.mime.MultipartEntityBuilder;
052import org.apache.http.entity.mime.content.InputStreamBody;
053import org.apache.http.entity.mime.content.StringBody;
054import org.apache.http.impl.client.CloseableHttpClient;
055import org.apache.http.impl.client.HttpClientBuilder;
056import org.apache.http.message.BasicNameValuePair;
057
058import org.ametys.core.authentication.CredentialProvider;
059import org.ametys.core.user.UserIdentity;
060import org.ametys.core.util.URLEncoder;
061import org.ametys.plugins.site.Site;
062import org.ametys.plugins.site.SiteUrl;
063import org.ametys.plugins.site.headers.RequestHeader;
064import org.ametys.plugins.site.headers.RequestHeaderExtensionPoint;
065import org.ametys.runtime.config.Config;
066
067/**
068 * Helper class that builds the request the front-office makes to the back-office to query a page or a resource.
069 */
070public final class BackOfficeRequestHelper
071{
072    private static final Pattern __AUTHORIZED_HEADERS = Pattern.compile("^(?:Accept|Accept-Language|Accept-Charset|Referer|Origin|User-Agent)$", Pattern.CASE_INSENSITIVE);
073    
074    private static final Set<String> __FILTERED_REQUEST_PARAMETERS = new HashSet<>(Arrays.asList("cocoon-view"));
075    
076    private BackOfficeRequestHelper()
077    {
078        // Helper class.
079    }
080    
081    /**
082     * Build a HttpClient object parametrized
083     * @return The httpclient object
084     */
085    public static CloseableHttpClient getHttpClient()
086    {
087        CloseableHttpClient httpClient = HttpClientBuilder.create()
088            .disableRedirectHandling()
089            .useSystemProperties()
090            .build();
091        
092        return httpClient;
093    }
094    
095    
096    /**
097     * Build a HttpClient request object that will be sent to the back-office to query the page.
098     * @param objectModel the current object model.
099     * @param page the wanted page path.
100     * @param requestHeaderEP The extension point for adding request headers in BO request
101     * @return the HttpClient request, to be sent to the back-office.
102     * @throws IOException if an error occurs building the request.
103     */
104    public static HttpUriRequest getRequest(Map objectModel, String page, RequestHeaderExtensionPoint requestHeaderEP) throws IOException
105    {
106        Request request = ObjectModelHelper.getRequest(objectModel);
107        
108        String cmsURL = Config.getInstance().getValueAsString("org.ametys.site.bo");
109        
110        String method = request.getMethod();
111        SiteUrl url = (SiteUrl)  request.getAttribute("url");
112        
113        String baseServerPath = url.getBaseServerPath(request);
114        
115        HttpUriRequest boRequest = null;
116        
117        if ("GET".equals(method))
118        {
119            boRequest = new HttpGet(cmsURL + "/generate/" + page + "?_contextPath=" + url.getServerPath() + "&_baseServerPath=" + baseServerPath + "&_initialRequest=" + URLEncoder.encodeParameter("/" + request.getAttribute("path") + (StringUtils.isEmpty(request.getQueryString()) ? "" : "?" + request.getQueryString())) + _getParameters(request));
120        }
121        else if ("POST".equals(method))
122        {
123            HttpServletRequest req = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT);
124            InputStream body = req.getInputStream();
125            byte[] bytes = IOUtils.toByteArray(body);
126            
127            boolean hasBody = bytes.length > 0;
128            
129            String uri = cmsURL + "/generate/" + page;
130            
131            if (hasBody)
132            {
133                // in case of a body in the original request, we have to copy this body, and put the two parameters _contextPath and _baseServerPath as query string
134                uri += "?_contextPath=" + url.getServerPath() + "&_baseServerPath=" + baseServerPath;
135            }
136            
137            HttpPost postRequest = new HttpPost(uri);
138            
139            HttpEntity entity = null;
140            
141            String contentType = request.getContentType();
142            if ((contentType != null) && (contentType.toLowerCase().indexOf("multipart/form-data") > -1))
143            {
144                // multipart request
145                MultipartEntityBuilder multipartBuilder = _getMultipartEntityBuilder(request);
146                
147                multipartBuilder.addPart("_contextPath", new StringBody(url.getServerPath(), ContentType.create("text/plain", Consts.UTF_8)));
148                multipartBuilder.addPart("_baseServerPath", new StringBody(baseServerPath, ContentType.create("text/plain", Consts.UTF_8)));
149                
150                entity = multipartBuilder.build();
151            }
152            else if (hasBody)
153            {
154                postRequest.setHeader("Content-Type", contentType);
155                entity = new InputStreamEntity(new ByteArrayInputStream(bytes), bytes.length);
156            }
157            else
158            {
159                // url encoded body
160                List<NameValuePair> params = new ArrayList<>();
161                
162                params.add(new BasicNameValuePair("_contextPath", url.getServerPath()));
163                params.add(new BasicNameValuePair("_baseServerPath", baseServerPath));
164                
165                Enumeration<String> names = request.getParameterNames();
166                while (names.hasMoreElements())
167                {
168                    String paramName = names.nextElement();
169                    if (!__FILTERED_REQUEST_PARAMETERS.contains(paramName))
170                    {
171                        for (String value : request.getParameterValues(paramName))
172                        {
173                            params.add(new BasicNameValuePair(paramName, value));
174                        }
175                    }
176                }
177                
178                entity = new UrlEncodedFormEntity(params, "UTF-8");
179            }
180            
181            postRequest.setEntity(entity);
182            
183            boRequest = postRequest;
184        }
185        else
186        {
187            throw new IllegalArgumentException("Only GET and POST methods are allowed.");
188        }
189        
190        _addRequestHeaders(request, boRequest, requestHeaderEP);
191        _copyCookieHeaders(request, boRequest);
192        
193        return boRequest;
194    }
195    
196    /**
197     * Get the front-office request's parameters as an HttpClient MultipartEntity,
198     * to be added to a POST back-office request.
199     * @param request the front-office request.
200     * @return the parameters encoded in a multipart entity.
201     * @throws IOException if an error occurs extracting the parameters or building the entity.
202     */
203    private static MultipartEntityBuilder _getMultipartEntityBuilder(Request request) throws IOException
204    {
205        MultipartEntityBuilder builder = MultipartEntityBuilder.create();
206        builder.setMode(HttpMultipartMode.RFC6532);
207        
208        Enumeration<String> names = request.getParameterNames();
209        
210        while (names.hasMoreElements())
211        {
212            String paramName = names.nextElement();
213            
214            if (!__FILTERED_REQUEST_PARAMETERS.contains(paramName))
215            {
216                Object value = request.get(paramName);
217                _addMultipartEntityToBuilder(builder, paramName, value);
218            }
219        }
220        
221        return builder;
222    }
223
224    private static void _addMultipartEntityToBuilder(MultipartEntityBuilder builder, String paramName, Object value) throws IOException
225    {
226        if (value instanceof Part)
227        {
228            Part part = (Part) value;
229            builder.addPart(paramName, new InputStreamBody(part.getInputStream(), ContentType.create(part.getMimeType()), part.getFileName()));
230        }
231        else if (value instanceof Vector)
232        {
233            for (Object v : (Vector) value)
234            {
235                _addMultipartEntityToBuilder(builder, paramName, v);
236            }
237        }
238        else
239        {
240            builder.addPart(paramName, new StringBody(value.toString(), ContentType.create("text/plain", Consts.UTF_8)));
241        }
242    }
243    
244    /**
245     * Get the front-office request's parameters as a String, to be added to
246     * a GET back-office request.
247     * @param request the front-office request.
248     * @return the parameters as a String.
249     */
250    private static String _getParameters(Request request)
251    {
252        StringBuilder params = new StringBuilder();
253        
254        Enumeration<String> names = request.getParameterNames();
255        while (names.hasMoreElements())
256        {
257            String paramName = names.nextElement();
258            if (!__FILTERED_REQUEST_PARAMETERS.contains(paramName))
259            {
260                for (String value : request.getParameterValues(paramName))
261                {
262                    params.append("&");
263                    params.append(paramName);
264                    params.append("=");
265                    params.append(URLEncoder.encodeParameter(value));
266                }
267            }
268        }
269        
270        return params.toString();
271    }
272    
273    /**
274     * Add headers indicating this is a request from the front-office to the back-office,
275     * specifying the user if applicable.
276     * @param request the front-office request.
277     * @param boRequest the request object to be sent to the back-office.
278     * @param requestHeaderEP The extension point for adding request headers in BO request
279     */
280    private static void _addRequestHeaders(Request request, HttpUriRequest boRequest, RequestHeaderExtensionPoint requestHeaderEP)
281    {
282        // Get the user, if in session.
283        UserIdentity user = FrontAuthenticateAction.getUserIdentityFromSession(request);
284        
285        // Add Ametys headers.
286        boRequest.addHeader("X-Ametys-FO", "true");
287        if (user != null)
288        {
289            boRequest.addHeader("X-Ametys-FO-Login", user.getLogin());
290            boRequest.addHeader("X-Ametys-FO-Population", user.getPopulationId());
291            
292            Site site = (Site) request.getAttribute("site");
293            Session session = request.getSession(false);
294            if (site != null && session != null)
295            {
296                CredentialProvider credentialProvider = (CredentialProvider) session.getAttribute("Runtime:CredentialProvider-" + site.getName());
297                boRequest.addHeader("X-Ametys-FO-Credential-Provider", credentialProvider.getId());
298            }
299            
300        }
301        
302        for (String requestHeaderId : requestHeaderEP.getExtensionsIds())
303        {
304            RequestHeader requestHeader = requestHeaderEP.getExtension(requestHeaderId);
305            for (Header header : requestHeader.getHeaders(request))
306            {
307                boRequest.addHeader(header);
308            }
309        }
310        
311        // Add apache unique-id
312        String uuid = (String) request.getAttribute("Monitoring-UUID");
313        if (uuid != null)
314        {
315            boRequest.addHeader("X-Ametys-FO-UUID", uuid);
316        }
317        
318        // Forwarding headers
319        Enumeration<String> headers = request.getHeaderNames();
320        while (headers.hasMoreElements())
321        {
322            String headerName = headers.nextElement();
323            if (__AUTHORIZED_HEADERS.matcher(headerName).matches())
324            {
325                // forward
326                Enumeration<String> headerValues = request.getHeaders(headerName);
327                while (headerValues.hasMoreElements())
328                {
329                    String headerValue = headerValues.nextElement();
330                    boRequest.addHeader(headerName, headerValue);
331                }
332            }
333        }
334        
335        // Add X-Forwarded-For
336        String xff = request.getHeader("X-Forwarded-For");
337        String remoteIP = request.getRemoteAddr();
338        
339        String newXFF = (xff == null ? "" : xff + ", ") + remoteIP;
340        boRequest.setHeader("X-Forwarded-For", newXFF);
341    }
342    
343    /**
344     * Copy cookie headers that were sent by the client into the request
345     * that will be sent to the back-office.
346     * @param request the front-office request.
347     * @param boRequest the request object to be sent to the back-office.
348     */
349    private static void _copyCookieHeaders(Request request, HttpUriRequest boRequest)
350    {
351        Enumeration<String> cookieHeaders = request.getHeaders("Cookie");
352        while (cookieHeaders.hasMoreElements())
353        {
354            String cookieHeader = cookieHeaders.nextElement();
355            
356            String[] cookiesValue = cookieHeader.split("; ");
357            for (String cookieValue : cookiesValue)
358            {
359                if (cookieValue.startsWith("JSESSIONID="))
360                {
361                    // discard (the BO should not try to attach a session with this id)
362                }
363                else if (cookieValue.startsWith(GeneratePageAction.__BACKOFFICE_JSESSION_ID + "="))
364                {
365                    String modifiedCookieValue = cookieValue.replace(GeneratePageAction.__BACKOFFICE_JSESSION_ID + "=", "JSESSIONID=");
366                    boRequest.addHeader("Cookie", modifiedCookieValue);
367                }
368                else
369                {
370                    boRequest.addHeader("Cookie", cookieValue);
371                }
372            }
373        }
374    }
375}