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}