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