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.net.URI;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Enumeration;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.Vector;
030import java.util.regex.Pattern;
031
032import javax.servlet.http.HttpServletRequest;
033
034import org.apache.cocoon.environment.ObjectModelHelper;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.Session;
037import org.apache.cocoon.environment.http.HttpEnvironment;
038import org.apache.cocoon.servlet.multipart.Part;
039import org.apache.commons.io.IOUtils;
040import org.apache.commons.lang.StringUtils;
041import org.apache.http.Consts;
042import org.apache.http.HttpEntity;
043import org.apache.http.NameValuePair;
044import org.apache.http.client.entity.UrlEncodedFormEntity;
045import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
046import org.apache.http.client.methods.HttpGet;
047import org.apache.http.client.methods.HttpHead;
048import org.apache.http.client.methods.HttpOptions;
049import org.apache.http.client.methods.HttpPost;
050import org.apache.http.client.methods.HttpPut;
051import org.apache.http.client.methods.HttpRequestBase;
052import org.apache.http.client.methods.HttpUriRequest;
053import org.apache.http.entity.ContentType;
054import org.apache.http.entity.InputStreamEntity;
055import org.apache.http.entity.mime.HttpMultipartMode;
056import org.apache.http.entity.mime.MultipartEntityBuilder;
057import org.apache.http.entity.mime.content.InputStreamBody;
058import org.apache.http.entity.mime.content.StringBody;
059import org.apache.http.impl.client.CloseableHttpClient;
060import org.apache.http.impl.client.HttpClientBuilder;
061import org.apache.http.message.BasicNameValuePair;
062
063import org.ametys.core.authentication.CredentialProvider;
064import org.ametys.core.user.UserIdentity;
065import org.ametys.core.util.URLEncoder;
066import org.ametys.plugins.site.Site;
067import org.ametys.plugins.site.SiteUrl;
068import org.ametys.plugins.site.proxy.BackOfficeRequestProxy;
069import org.ametys.plugins.site.proxy.BackOfficeRequestProxyExtensionPoint;
070import org.ametys.runtime.config.Config;
071
072/**
073 * Helper class that builds the request the front-office makes to the back-office to query a page or a resource.
074 */
075public final class BackOfficeRequestHelper
076{
077    private static final Pattern __AUTHORIZED_HEADERS = Pattern.compile("^(?:Accept|Accept-Language|Accept-Charset|Referer|Origin|User-Agent|If-None-Match)$", Pattern.CASE_INSENSITIVE);
078    
079    private static final Set<String> __FILTERED_REQUEST_PARAMETERS = new HashSet<>(Arrays.asList("cocoon-view"));
080    
081    private BackOfficeRequestHelper()
082    {
083        // Helper class.
084    }
085    
086    /**
087     * Build a HttpClient object parametrized
088     * @return The httpclient object
089     */
090    public static CloseableHttpClient getHttpClient()
091    {
092        CloseableHttpClient httpClient = HttpClientBuilder.create()
093            .disableRedirectHandling()
094            .useSystemProperties()
095            .build();
096        
097        return httpClient;
098    }
099    
100    
101    /**
102     * Build a HttpClient request object that will be sent to the back-office to query the page.
103     * @param objectModel the current object model.
104     * @param page the wanted page path.
105     * @param requestProxyExtensionPoint The extension point for adding request headers in BO request
106     * @return the HttpClient request, to be sent to the back-office.
107     * @throws IOException if an error occurs building the request.
108     */
109    public static HttpUriRequest getRequest(Map objectModel, String page, BackOfficeRequestProxyExtensionPoint requestProxyExtensionPoint) throws IOException
110    {
111        Request request = ObjectModelHelper.getRequest(objectModel);
112        
113        String cmsURL = Config.getInstance().getValueAsString("org.ametys.site.bo");
114        
115        String method = request.getMethod();
116        SiteUrl url = (SiteUrl)  request.getAttribute("url");
117        
118        String baseServerPath = url.getBaseServerPath(request);
119        
120        HttpUriRequest boRequest = null;
121        String baseUrl = cmsURL + "/generate/" + page;
122        
123        switch (method)
124        {
125            case "GET":
126            case "HEAD":
127            case "OPTIONS":
128            case "UNLOCK":
129                String boUrl = baseUrl + "?" + _getContextQueryPart(url, baseServerPath) 
130                                       + "&_initialRequest=" + URLEncoder.encodeParameter("/" + request.getAttribute("path") + (StringUtils.isEmpty(request.getQueryString()) ? "" : "?" + request.getQueryString())) + _getParameters(request) 
131                                       + "&" + _getEditionQueryPart(request);
132                
133                if ("GET".equals(method))
134                {
135                    boRequest = new HttpGet(boUrl);
136                }
137                else if ("HEAD".equals(method))
138                {
139                    boRequest = new HttpHead(boUrl);
140                }
141                else if ("OPTIONS".equals(method))
142                {
143                    boRequest = new HttpOptions(boUrl);
144                }
145                else if ("UNLOCK".equals(method))
146                {
147                    boRequest = new HttpUnLock(boUrl);
148                }
149                
150                break;
151            case "PUT":
152            case "LOCK":
153            case "PROPFIND":
154            case "MKCOL":
155                String uri = baseUrl + "?" + _getContextQueryPart(url, baseServerPath) + "&" + _getEditionQueryPart(request);
156                
157                HttpEntityEnclosingRequestBase newRequest = null;
158                
159                if ("PUT".equals(method))
160                {
161                    newRequest = new HttpPut(uri);
162                }
163                else if ("LOCK".equals(method))
164                {
165                    newRequest = new HttpLock(uri);
166                }
167                else if ("PROPFIND".equals(method))
168                {
169                    newRequest = new HttpPropfind(uri);
170                }
171                else if ("MKCOL".equals(method))
172                {
173                    newRequest = new HttpMkcol(uri);
174                }
175
176                assert newRequest != null;
177                
178                String contentType = request.getContentType();
179                if (contentType != null)
180                {
181                    newRequest.setHeader("Content-Type", contentType);
182                }
183                
184                HttpServletRequest req = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT);
185                InputStream body = req.getInputStream();
186                byte[] bytes = IOUtils.toByteArray(body);
187                
188                HttpEntity entity = new InputStreamEntity(new ByteArrayInputStream(bytes), bytes.length);
189                newRequest.setEntity(entity);
190                
191                boRequest = newRequest;
192                break;
193            case "POST":
194                HttpServletRequest postReq = (HttpServletRequest) objectModel.get(HttpEnvironment.HTTP_REQUEST_OBJECT);
195                InputStream postBody = postReq.getInputStream();
196                byte[] postBytes = IOUtils.toByteArray(postBody);
197                
198                boolean hasBody = postBytes.length > 0;
199                
200                String postUri = baseUrl;
201                
202                if (hasBody)
203                {
204                    // 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
205                    postUri += "?" + _getContextQueryPart(url, baseServerPath) + "&" + _getEditionQueryPart(request);
206                }
207                else
208                {
209                    postUri += "?" + _getEditionQueryPart(request);
210                }
211                
212                HttpPost postRequest = new HttpPost(postUri);
213                
214                HttpEntity postEntity = null;
215                
216                String postContentType = request.getContentType();
217                if ((postContentType != null) && (postContentType.toLowerCase().indexOf("multipart/form-data") > -1))
218                {
219                    // multipart request
220                    MultipartEntityBuilder multipartBuilder = _getMultipartEntityBuilder(request);
221                    
222                    multipartBuilder.addPart("_contextPath", new StringBody(url.getServerPath(), ContentType.create("text/plain", Consts.UTF_8)));
223                    multipartBuilder.addPart("_baseServerPath", new StringBody(baseServerPath, ContentType.create("text/plain", Consts.UTF_8)));
224                    
225                    postEntity = multipartBuilder.build();
226                }
227                else if (hasBody)
228                {
229                    postRequest.setHeader("Content-Type", postContentType);
230                    postEntity = new InputStreamEntity(new ByteArrayInputStream(postBytes), postBytes.length);
231                }
232                else
233                {
234                    // url encoded body
235                    List<NameValuePair> params = new ArrayList<>();
236                    
237                    params.add(new BasicNameValuePair("_contextPath", url.getServerPath()));
238                    params.add(new BasicNameValuePair("_baseServerPath", baseServerPath));
239                    
240                    Enumeration<String> names = request.getParameterNames();
241                    while (names.hasMoreElements())
242                    {
243                        String paramName = names.nextElement();
244                        if (!__FILTERED_REQUEST_PARAMETERS.contains(paramName))
245                        {
246                            for (String value : request.getParameterValues(paramName))
247                            {
248                                params.add(new BasicNameValuePair(paramName, value));
249                            }
250                        }
251                    }
252                    
253                    postEntity = new UrlEncodedFormEntity(params, "UTF-8");
254                }
255                
256                postRequest.setEntity(postEntity);
257                
258                boRequest = postRequest;
259                
260                break;
261            default:
262                throw new IllegalArgumentException("Unrecognized method " + method);
263        }
264        
265        _addRequestHeaders(request, boRequest, requestProxyExtensionPoint);
266        _copyCookieHeaders(request, boRequest);
267        
268        return boRequest;
269    }
270    
271    private static String _getEditionQueryPart(Request request)
272    {
273        String editionMode = (String) request.getAttribute(GetSiteAction.EDITION_URI);
274        return "true".equals(editionMode) ? "_" + GetSiteAction.EDITION_URI + "=true" : "";
275    }
276    
277    private static String _getContextQueryPart(SiteUrl url, String baseServerPath)
278    {
279        return "_contextPath=" + url.getServerPath() + "&_baseServerPath=" + baseServerPath;
280    }
281    
282    /**
283     * Get the front-office request's parameters as an HttpClient MultipartEntity,
284     * to be added to a POST back-office request.
285     * @param request the front-office request.
286     * @return the parameters encoded in a multipart entity.
287     * @throws IOException if an error occurs extracting the parameters or building the entity.
288     */
289    private static MultipartEntityBuilder _getMultipartEntityBuilder(Request request) throws IOException
290    {
291        MultipartEntityBuilder builder = MultipartEntityBuilder.create();
292        builder.setMode(HttpMultipartMode.RFC6532);
293        
294        Enumeration<String> names = request.getParameterNames();
295        
296        while (names.hasMoreElements())
297        {
298            String paramName = names.nextElement();
299            
300            if (!__FILTERED_REQUEST_PARAMETERS.contains(paramName))
301            {
302                Object value = request.get(paramName);
303                _addMultipartEntityToBuilder(builder, paramName, value);
304            }
305        }
306        
307        return builder;
308    }
309
310    private static void _addMultipartEntityToBuilder(MultipartEntityBuilder builder, String paramName, Object value) throws IOException
311    {
312        if (value instanceof Part)
313        {
314            Part part = (Part) value;
315            builder.addPart(paramName, new InputStreamBody(part.getInputStream(), ContentType.create(part.getMimeType()), part.getFileName()));
316        }
317        else if (value instanceof Vector)
318        {
319            for (Object v : (Vector) value)
320            {
321                _addMultipartEntityToBuilder(builder, paramName, v);
322            }
323        }
324        else
325        {
326            builder.addPart(paramName, new StringBody(value.toString(), ContentType.create("text/plain", Consts.UTF_8)));
327        }
328    }
329    
330    /**
331     * Get the front-office request's parameters as a String, to be added to
332     * a GET back-office request.
333     * @param request the front-office request.
334     * @return the parameters as a String.
335     */
336    private static String _getParameters(Request request)
337    {
338        StringBuilder params = new StringBuilder();
339        
340        Enumeration<String> names = request.getParameterNames();
341        while (names.hasMoreElements())
342        {
343            String paramName = names.nextElement();
344            if (!__FILTERED_REQUEST_PARAMETERS.contains(paramName))
345            {
346                for (String value : request.getParameterValues(paramName))
347                {
348                    params.append("&");
349                    params.append(paramName);
350                    params.append("=");
351                    params.append(URLEncoder.encodeParameter(value));
352                }
353            }
354        }
355        
356        return params.toString();
357    }
358    
359    /**
360     * Add headers indicating this is a request from the front-office to the back-office,
361     * specifying the user if applicable.
362     * @param request the front-office request.
363     * @param boRequest the request object to be sent to the back-office.
364     * @param requestProxyExtensionPoint The extension point for adding request headers in BO request
365     */
366    private static void _addRequestHeaders(Request request, HttpUriRequest boRequest, BackOfficeRequestProxyExtensionPoint requestProxyExtensionPoint)
367    {
368        // Get the user, if in session.
369        UserIdentity user = FrontAuthenticateAction.getUserIdentityFromSession(request);
370        
371        // Add Ametys headers.
372        boRequest.addHeader("X-Ametys-FO", "true");
373        if (user != null)
374        {
375            boRequest.addHeader("X-Ametys-FO-Login", user.getLogin());
376            boRequest.addHeader("X-Ametys-FO-Population", user.getPopulationId());
377            
378            Site site = (Site) request.getAttribute("site");
379            Session session = request.getSession(false);
380            if (site != null && session != null)
381            {
382                CredentialProvider credentialProvider = (CredentialProvider) session.getAttribute("Runtime:CredentialProvider-" + site.getName());
383                boRequest.addHeader("X-Ametys-FO-Credential-Provider", credentialProvider.getId());
384            }
385            
386        }
387        
388        for (String requestId : requestProxyExtensionPoint.getExtensionsIds())
389        {
390            BackOfficeRequestProxy boRequestComponent = requestProxyExtensionPoint.getExtension(requestId);
391            boRequestComponent.prepareBackOfficeRequest(request, boRequest);
392        }
393        
394        // Add apache unique-id
395        String uuid = (String) request.getAttribute("Monitoring-UUID");
396        if (uuid != null)
397        {
398            boRequest.addHeader("X-Ametys-FO-UUID", uuid);
399        }
400        
401        // Forwarding headers
402        Enumeration<String> headers = request.getHeaderNames();
403        while (headers.hasMoreElements())
404        {
405            String headerName = headers.nextElement();
406            if (__AUTHORIZED_HEADERS.matcher(headerName).matches())
407            {
408                // forward
409                Enumeration<String> headerValues = request.getHeaders(headerName);
410                while (headerValues.hasMoreElements())
411                {
412                    String headerValue = headerValues.nextElement();
413                    boRequest.addHeader(headerName, headerValue);
414                }
415            }
416        }
417        
418        // Add X-Forwarded-For
419        String xff = request.getHeader("X-Forwarded-For");
420        String remoteIP = request.getRemoteAddr();
421        
422        String newXFF = (xff == null ? "" : xff + ", ") + remoteIP;
423        boRequest.setHeader("X-Forwarded-For", newXFF);
424    }
425    
426    /**
427     * Copy cookie headers that were sent by the client into the request
428     * that will be sent to the back-office.
429     * @param request the front-office request.
430     * @param boRequest the request object to be sent to the back-office.
431     */
432    private static void _copyCookieHeaders(Request request, HttpUriRequest boRequest)
433    {
434        Enumeration<String> cookieHeaders = request.getHeaders("Cookie");
435        while (cookieHeaders.hasMoreElements())
436        {
437            String cookieHeader = cookieHeaders.nextElement();
438            
439            String[] cookiesValue = cookieHeader.split("; ");
440            for (String cookieValue : cookiesValue)
441            {
442                if (cookieValue.startsWith("JSESSIONID="))
443                {
444                    // discard (the BO should not try to attach a session with this id)
445                }
446                else if (cookieValue.startsWith(GeneratePageAction.__BACKOFFICE_JSESSION_ID + "="))
447                {
448                    String modifiedCookieValue = cookieValue.replace(GeneratePageAction.__BACKOFFICE_JSESSION_ID + "=", "JSESSIONID=");
449                    boRequest.addHeader("Cookie", modifiedCookieValue);
450                }
451                else
452                {
453                    boRequest.addHeader("Cookie", cookieValue);
454                }
455            }
456        }
457    }
458    
459    static class HttpLock extends HttpEntityEnclosingRequestBase 
460    {
461        HttpLock(final String uri) 
462        {
463            super();
464            setURI(URI.create(uri));
465        }
466
467        @Override
468        public String getMethod() 
469        {
470            return "LOCK";
471        }
472    }
473    
474    static class HttpUnLock extends HttpRequestBase 
475    {
476        HttpUnLock(final String uri) 
477        {
478            super();
479            setURI(URI.create(uri));
480        }
481
482        @Override
483        public String getMethod() 
484        {
485            return "UNLOCK";
486        }
487    }
488    
489    static class HttpPropfind extends HttpEntityEnclosingRequestBase 
490    {
491        HttpPropfind(final String uri) 
492        {
493            super();
494            setURI(URI.create(uri));
495        }
496
497        @Override
498        public String getMethod() 
499        {
500            return "PROPFIND";
501        }
502    }
503    
504    static class HttpMkcol extends HttpEntityEnclosingRequestBase
505    {
506        HttpMkcol(final String uri)
507        {
508            super();
509            setURI(URI.create(uri));
510        }
511
512        @Override
513        public String getMethod()
514        {
515            return "MKCOL";
516        }
517    }
518}