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