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}