001/* 002 * Copyright 2010 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.File; 019import java.io.FileOutputStream; 020import java.io.IOException; 021import java.net.HttpCookie; 022import java.util.Enumeration; 023import java.util.List; 024import java.util.Map; 025 026import org.apache.avalon.framework.parameters.Parameters; 027import org.apache.avalon.framework.service.ServiceException; 028import org.apache.avalon.framework.service.ServiceManager; 029import org.apache.avalon.framework.thread.ThreadSafe; 030import org.apache.cocoon.ProcessingException; 031import org.apache.cocoon.ResourceNotFoundException; 032import org.apache.cocoon.acting.ServiceableAction; 033import org.apache.cocoon.environment.Cookie; 034import org.apache.cocoon.environment.ObjectModelHelper; 035import org.apache.cocoon.environment.PermanentRedirector; 036import org.apache.cocoon.environment.Redirector; 037import org.apache.cocoon.environment.Request; 038import org.apache.cocoon.environment.Response; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; 041import org.apache.hc.core5.http.ClassicHttpRequest; 042import org.apache.hc.core5.http.ClassicHttpResponse; 043import org.apache.hc.core5.http.Header; 044import org.apache.hc.core5.http.HttpEntity; 045import org.apache.hc.core5.http.HttpResponse; 046 047import org.ametys.core.user.UserIdentity; 048import org.ametys.core.util.URIUtils; 049import org.ametys.plugins.site.Site; 050import org.ametys.plugins.site.SiteUrl; 051import org.ametys.plugins.site.proxy.BackOfficeRequestProxy; 052import org.ametys.plugins.site.proxy.BackOfficeRequestProxyExtensionPoint; 053import org.ametys.runtime.authentication.AccessDeniedException; 054import org.ametys.runtime.exception.ServiceUnavailableException; 055 056/** 057 * Call the BO for getting a page content and stores it in the local cache. 058 */ 059public class GeneratePageAction extends ServiceableAction implements ThreadSafe 060{ 061 static final String __BACKOFFICE_JSESSION_ID = "JSESSIONID-Ametys"; 062 063 private CacheAccessManager _cacheAccess; 064 private CacheAccessCounter _cacheAccessCounter; 065 private BackOfficeRequestProxyExtensionPoint _requestHeaderEP; 066 067 @Override 068 public void service(ServiceManager sManager) throws ServiceException 069 { 070 super.service(sManager); 071 _requestHeaderEP = (BackOfficeRequestProxyExtensionPoint) sManager.lookup(BackOfficeRequestProxyExtensionPoint.ROLE); 072 } 073 074 /** 075 * Get the cache access counter 076 * @return the CacheAccessCounter 077 */ 078 protected CacheAccessCounter _getCacheAccessCounter() 079 { 080 if (_cacheAccessCounter == null) 081 { 082 try 083 { 084 _cacheAccessCounter = (CacheAccessCounter) manager.lookup(CacheAccessCounter.ROLE); 085 } 086 catch (ServiceException e) 087 { 088 throw new IllegalStateException("Cannot get CacheAccessCounter", e); 089 } 090 } 091 return _cacheAccessCounter; 092 } 093 094 /** 095 * Get the cache access manager 096 * @return the CacheAccessManager 097 */ 098 protected CacheAccessManager _getCacheAccessManager() 099 { 100 if (_cacheAccess == null) 101 { 102 try 103 { 104 _cacheAccess = (CacheAccessManager) manager.lookup(CacheAccessManager.ROLE); 105 } 106 catch (ServiceException e) 107 { 108 throw new IllegalStateException("Cannot get CacheAccessManager", e); 109 } 110 } 111 return _cacheAccess; 112 } 113 114 @Override 115 public Map act(Redirector redirector, org.apache.cocoon.environment.SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 116 { 117 Request request = ObjectModelHelper.getRequest(objectModel); 118 Response response = ObjectModelHelper.getResponse(objectModel); 119 120 String page = parameters.getParameter("page"); 121 _log("Generating the resource " + page); 122 123 Site site = (Site) request.getAttribute("site"); 124 if (site != null) 125 { 126 _getCacheAccessCounter().increaseAskedResources(site.getName()); 127 } 128 129 CloseableHttpClient httpClient = null; 130 try 131 { 132 // If the request was previously done (to test cachability), get it back 133 httpClient = (CloseableHttpClient) request.getAttribute("http-client"); 134 ClassicHttpRequest cmsRequest = (ClassicHttpRequest) request.getAttribute("cms-request"); 135 ClassicHttpResponse cmsResponse = (ClassicHttpResponse) request.getAttribute("cms-response"); 136 137 // If page was already known as non cacheable, we should do the request by now 138 if (cmsResponse == null) 139 { 140 // FIXME CMS-12715 Reuse the same client for all request from site to cms 141 httpClient = BackOfficeRequestHelper.getHttpClient(); 142 cmsRequest = BackOfficeRequestHelper.getRequest(objectModel, page, _requestHeaderEP); 143 cmsResponse = httpClient.executeOpen(null, cmsRequest, null); 144 145 request.setAttribute("cms-request", cmsRequest); 146 request.setAttribute("cms-response", cmsResponse); 147 request.setAttribute("http-client", httpClient); 148 } 149 150 // Copy some headers in the response. 151 _copyHeaders(request, response, cmsResponse, new String[]{"Content-Disposition", "Accept-Ranges", "Content-Range", "Content-Type", "Content-Length", "Cache-Control", "Allow", "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials", "ETag", "Last-Modified", "Ametys-Dispatched"}); 152 153 for (String requestId : _requestHeaderEP.getExtensionsIds()) 154 { 155 BackOfficeRequestProxy boRequestComponent = _requestHeaderEP.getExtension(requestId); 156 boRequestComponent.handleBackOfficeResponse(response, cmsResponse); 157 } 158 159 // Handle the request 160 int statusCode = cmsResponse.getCode(); 161 switch (statusCode) 162 { 163 case 200: 164 Header header = cmsResponse.getFirstHeader("X-Ametys-Cacheable"); 165 Object editionMode = request.getAttribute(GetSiteAction.EDITION_URI); 166 if (header != null && "true".equals(header.getValue()) && !"true".equals(editionMode)) 167 { 168 _writePageOnDisk(page, cmsResponse); 169 170 _log("Succeed to generate cacheable resource '" + page + "'"); 171 172 return null; 173 } 174 175 // Set httpClient to null because we don't want it to be shut down in the finally statement, but by the CMSResponseReader to follow 176 httpClient = null; 177 _log("Succeed to generate uncacheable resource '" + page + "'"); 178 179 return EMPTY_MAP; 180 case 204: 181 case 206: 182 case 207: 183 case 304: 184 ((org.apache.cocoon.environment.http.HttpResponse) response).setStatus(statusCode); 185 186 // Set httpClient to null because we don't want it to be shut down in the finally statement, but by the CMSResponseReader to follow 187 httpClient = null; 188 return EMPTY_MAP; 189 190 case 301: // Permanent redirection 191 _redirect(cmsResponse, redirector, page, true); 192 return null; 193 194 case 302: // Redirection 195 _redirect(cmsResponse, redirector, page, false); 196 return null; 197 198 case 401: // authorization required 199 if (site == null) 200 { 201 throw new IllegalStateException("Cannot authenticate outsite a site"); 202 } 203 204 SiteUrl siteUrl = site.getSiteUrls().get(0); 205 206 redirector.redirect(false, siteUrl.getBaseServerPath(request) + siteUrl.getServerPath() + "/_authenticate?requestedURL=" + _encodeRequestedUrl(request)); 207 _log("Resource '" + page + "' needs authentication"); 208 return null; 209 210 case 403: // access denied 211 UserIdentity user = site != null ? FrontAuthenticateAction.getUserIdentityFromSession(request, site.getName()) : null; 212 String userStr = user != null ? "user " + user.toString() : " anonymous user"; 213 214 _log("Access denied for resource '" + page + "' for " + userStr); 215 throw new AccessDeniedException("Access denied for " + userStr + " for URL " + cmsRequest.getUri()); 216 217 case 404: // not found 218 _log("Resource not found '" + page + "'"); 219 throw new ResourceNotFoundException("Resource not found for URL " + cmsRequest.getUri()); 220 221 case 503: // Incomplete configuration or site down. 222 _log("Site down for URL '" + page + "'"); 223 BackOfficeRequestHelper.switchOnMaintenanceIfNeeded(cmsResponse); 224 throw new ServiceUnavailableException("Site down for URL " + cmsRequest.getUri()); 225 226 default: 227 _log("Unable to get resource '" + page + "'. Status code is " + statusCode); 228 throw new ProcessingException("Unable to get URL '" + page + "' at URL '" + cmsRequest.getUri() + "'. Status code is " + statusCode); 229 } 230 } 231 finally 232 { 233 // Whatever happens, unlock the page. 234 _getCacheAccessManager().unlock(page); 235 236 if (httpClient != null) 237 { 238 httpClient.close(); 239 } 240 } 241 } 242 243 private String _encodeRequestedUrl(Request request) 244 { 245 String requestedURI = (String) request.getAttribute("requestedURI"); 246 247 // Transmit parameters 248 StringBuilder transmittedParameters = new StringBuilder(); 249 boolean first = true; 250 Enumeration<String> parameterNames = request.getParameterNames(); 251 252 while (parameterNames.hasMoreElements()) 253 { 254 if (first) 255 { 256 transmittedParameters.append("?"); 257 first = false; 258 } 259 else 260 { 261 transmittedParameters.append("&"); 262 } 263 String parameterName = parameterNames.nextElement(); 264 transmittedParameters.append(parameterName); 265 transmittedParameters.append("="); 266 transmittedParameters.append(URIUtils.encodeParameter(request.getParameter(parameterName))); 267 } 268 269 if (!first) 270 { 271 // request.getAttribute("requestedURI") is already encoded 272 requestedURI += URIUtils.encodeParameter(transmittedParameters.toString()); 273 } 274 275 return requestedURI; 276 } 277 278 private void _log(String message) 279 { 280 if (getLogger().isDebugEnabled()) 281 { 282 getLogger().debug(message); 283 } 284 } 285 286 private void _redirect(HttpResponse cmsResponse, Redirector redirector, String page, boolean permanent) throws Exception 287 { 288 String location = cmsResponse.getFirstHeader("Location").getValue(); 289 290 if (permanent && redirector instanceof PermanentRedirector) 291 { 292 ((PermanentRedirector) redirector).permanentRedirect(false, location); 293 } 294 else 295 { 296 redirector.redirect(false, location); 297 } 298 299 _log("Redirect '" + page + "' to '" + location + "'"); 300 } 301 302 /** 303 * Copy some response headers from the back-office. 304 * @param request the front-office client request. 305 * @param response the front-office client response. 306 * @param cmsResponse the response from the back-office. 307 * @param names the header names. 308 */ 309 protected void _copyHeaders(Request request, Response response, HttpResponse cmsResponse, String[] names) 310 { 311 _transposeCookies(request, response, cmsResponse); 312 313 for (String name : names) 314 { 315 Header[] headers = cmsResponse.getHeaders(name); 316 for (Header header : headers) 317 { 318 String value = header.getValue(); 319 320 response.addHeader(name, value); 321 } 322 } 323 } 324 325 private void _transposeCookies(Request request, Response response, HttpResponse cmsResponse) 326 { 327 Header[] headers = cmsResponse.getHeaders("Set-Cookie"); 328 for (Header header : headers) 329 { 330 String value = header.getValue(); 331 332 List<HttpCookie> cookies = HttpCookie.parse(value); 333 for (HttpCookie cookie : cookies) 334 { 335 String cookieName = cookie.getName(); 336 337 if ("JSESSIONID".equals(cookieName)) 338 { 339 if (getLogger().isWarnEnabled()) 340 { 341 getLogger().warn("Receiving JSESSIONID cookie from the back-office."); 342 } 343 cookieName = __BACKOFFICE_JSESSION_ID; 344 } 345 346 Cookie newCookie = response.createCookie(cookieName, cookie.getValue()); 347 javax.servlet.http.Cookie newHttpCookie = ((org.apache.cocoon.environment.http.HttpCookie) newCookie).getServletCookie(); 348 newHttpCookie.setComment(cookie.getComment()); 349 if (cookie.getDomain() != null) 350 { 351 newHttpCookie.setDomain(cookie.getDomain()); 352 } 353 newHttpCookie.setMaxAge((int) cookie.getMaxAge()); 354 newHttpCookie.setPath(StringUtils.defaultIfEmpty(request.getContextPath(), "/")); 355 newHttpCookie.setSecure(request.isSecure()); 356 newHttpCookie.setHttpOnly(cookie.isHttpOnly()); 357 response.addCookie(newCookie); 358 } 359 } 360 } 361 362 /** 363 * Read the page from the back-office response and write it into the cache. 364 * @param decodedPage the page path. 365 * @param cmsResponse the back-office response, containing the page. 366 * @throws IOException if an error occurs writing the page into the cache. 367 */ 368 protected void _writePageOnDisk(String decodedPage, ClassicHttpResponse cmsResponse) throws IOException 369 { 370 if (getLogger().isDebugEnabled()) 371 { 372 getLogger().debug("The page is cacheable, writing into the cache: " + decodedPage); 373 } 374 375 File root = SiteCacheHelper.getRootCache(); 376 377 File file = getFile(root, decodedPage); 378 if (!file.exists() && cmsResponse != null && cmsResponse.getEntity() != null) 379 { 380 file.getParentFile().mkdirs(); 381 382 try (FileOutputStream fos = new FileOutputStream(file); 383 HttpEntity entity = cmsResponse.getEntity()) 384 { 385 entity.writeTo(fos); 386 } 387 catch (IOException e) 388 { 389 throw new IOException("Error writing the file '" + file.getAbsolutePath() + "' into the cache, check that the directory is writable.", e); 390 } 391 } 392 393 if (getLogger().isDebugEnabled()) 394 { 395 getLogger().debug("Page " + decodedPage + " written into the cache."); 396 } 397 } 398 399 /** 400 * Get the cache file to write for the corresponding page. 401 * @param root the cache root folder. 402 * @param pagePath the page path. 403 * @return a valid file. 404 */ 405 protected File getFile(File root, String pagePath) 406 { 407 File file = new File(root, pagePath); 408 409 if (!SiteCacheHelper.isValid(file)) 410 { 411 String validPath = SiteCacheHelper.getHashedFilePath(pagePath); 412 413 file = new File(root, validPath); 414 } 415 416 return file; 417 } 418 419}