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