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|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}