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 some headers in the response. 148 _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"}); 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 206: 179 case 207: 180 case 304: 181 ((org.apache.cocoon.environment.http.HttpResponse) response).setStatus(statusCode); 182 183 // Set httpClient to null because we don't want it to be shut down in the finally statement, but by the CMSResponseReader to follow 184 httpClient = null; 185 return EMPTY_MAP; 186 187 case 301: // Permanent redirection 188 _redirect(cmsResponse, redirector, page, true); 189 return null; 190 191 case 302: // Redirection 192 _redirect(cmsResponse, redirector, page, false); 193 return null; 194 195 case 401: // authorization required 196 if (site == null) 197 { 198 throw new IllegalStateException("Cannot authenticate outsite a site"); 199 } 200 201 SiteUrl siteUrl = site.getSiteUrls().get(0); 202 203 redirector.redirect(false, siteUrl.getBaseServerPath(request) + siteUrl.getServerPath() + "/_authenticate?requestedURL=" + _encodeRequestedUrl(request)); 204 _log("Resource '" + page + "' needs authentication"); 205 return null; 206 207 case 403: // access denied 208 UserIdentity user = site != null ? FrontAuthenticateAction.getUserIdentityFromSession(request, site.getName()) : null; 209 String userStr = user != null ? "user " + user.toString() : " anonymous user"; 210 211 _log("Access denied for resource '" + page + "' for " + userStr); 212 throw new AccessDeniedException("Access denied for " + userStr + " for URL " + cmsRequest.getURI()); 213 214 case 404: // not found 215 _log("Resource not found '" + page + "'"); 216 throw new ResourceNotFoundException("Resource not found for URL " + cmsRequest.getURI()); 217 218 case 503: // Incomplete configuration or site down. 219 _log("Site down for URL '" + page + "'"); 220 BackOfficeRequestHelper.switchOnMaintenanceIfNeeded(cmsResponse); 221 throw new ServiceUnavailableException("Site down for URL " + cmsRequest.getURI()); 222 223 default: 224 _log("Unable to get resource '" + page + "'. Status code is " + statusCode); 225 throw new ProcessingException("Unable to get URL '" + page + "' at URL '" + cmsRequest.getURI() + "'. Status code is " + statusCode); 226 } 227 } 228 finally 229 { 230 // Whatever happens, unlock the page. 231 _getCacheAccessManager().unlock(page); 232 233 if (httpClient != null) 234 { 235 httpClient.close(); 236 } 237 } 238 } 239 240 private String _encodeRequestedUrl(Request request) 241 { 242 String requestedURI = (String) request.getAttribute("requestedURI"); 243 244 // Transmit parameters 245 StringBuilder transmittedParameters = new StringBuilder(); 246 boolean first = true; 247 Enumeration<String> parameterNames = request.getParameterNames(); 248 249 while (parameterNames.hasMoreElements()) 250 { 251 if (first) 252 { 253 transmittedParameters.append("?"); 254 first = false; 255 } 256 else 257 { 258 transmittedParameters.append("&"); 259 } 260 String parameterName = parameterNames.nextElement(); 261 transmittedParameters.append(parameterName); 262 transmittedParameters.append("="); 263 transmittedParameters.append(URIUtils.encodeParameter(request.getParameter(parameterName))); 264 } 265 266 if (!first) 267 { 268 // request.getAttribute("requestedURI") is already encoded 269 requestedURI += URIUtils.encodeParameter(transmittedParameters.toString()); 270 } 271 272 return requestedURI; 273 } 274 275 private void _log(String message) 276 { 277 if (getLogger().isDebugEnabled()) 278 { 279 getLogger().debug(message); 280 } 281 } 282 283 private void _redirect(HttpResponse cmsResponse, Redirector redirector, String page, boolean permanent) throws Exception 284 { 285 String location = cmsResponse.getFirstHeader("Location").getValue(); 286 287 if (permanent && redirector instanceof PermanentRedirector) 288 { 289 ((PermanentRedirector) redirector).permanentRedirect(false, location); 290 } 291 else 292 { 293 redirector.redirect(false, location); 294 } 295 296 _log("Redirect '" + page + "' to '" + location + "'"); 297 } 298 299 /** 300 * Copy some response headers from the back-office. 301 * @param request the front-office client request. 302 * @param response the front-office client response. 303 * @param cmsResponse the response from the back-office. 304 * @param names the header names. 305 */ 306 protected void _copyHeaders(Request request, Response response, HttpResponse cmsResponse, String[] names) 307 { 308 _transposeCookies(request, response, cmsResponse); 309 310 for (String name : names) 311 { 312 Header[] headers = cmsResponse.getHeaders(name); 313 for (Header header : headers) 314 { 315 String value = header.getValue(); 316 317 response.addHeader(name, value); 318 } 319 } 320 } 321 322 private void _transposeCookies(Request request, Response response, HttpResponse cmsResponse) 323 { 324 Header[] headers = cmsResponse.getHeaders("Set-Cookie"); 325 for (Header header : headers) 326 { 327 String value = header.getValue(); 328 329 List<HttpCookie> cookies = HttpCookie.parse(value); 330 for (HttpCookie cookie : cookies) 331 { 332 String cookieName = cookie.getName(); 333 334 if ("JSESSIONID".equals(cookieName)) 335 { 336 if (getLogger().isWarnEnabled()) 337 { 338 getLogger().warn("Receiving JSESSIONID cookie from the back-office."); 339 } 340 cookieName = __BACKOFFICE_JSESSION_ID; 341 } 342 343 Cookie newCookie = response.createCookie(cookieName, cookie.getValue()); 344 javax.servlet.http.Cookie newHttpCookie = ((org.apache.cocoon.environment.http.HttpCookie) newCookie).getServletCookie(); 345 newHttpCookie.setComment(cookie.getComment()); 346 if (cookie.getDomain() != null) 347 { 348 newHttpCookie.setDomain(cookie.getDomain()); 349 } 350 newHttpCookie.setMaxAge((int) cookie.getMaxAge()); 351 newHttpCookie.setPath(StringUtils.defaultIfEmpty(request.getContextPath(), "/")); 352 newHttpCookie.setSecure(request.isSecure()); 353 newHttpCookie.setHttpOnly(cookie.isHttpOnly()); 354 response.addCookie(newCookie); 355 } 356 } 357 } 358 359 /** 360 * Read the page from the back-office response and write it into the cache. 361 * @param decodedPage the page path. 362 * @param cmsResponse the back-office response, containing the page. 363 * @throws IOException if an error occurs writing the page into the cache. 364 */ 365 protected void _writePageOnDisk(String decodedPage, HttpResponse cmsResponse) throws IOException 366 { 367 if (getLogger().isDebugEnabled()) 368 { 369 getLogger().debug("The page is cacheable, writing into the cache: " + decodedPage); 370 } 371 372 File root = SiteCacheHelper.getRootCache(); 373 374 File file = getFile(root, decodedPage); 375 if (!file.exists() && cmsResponse != null && cmsResponse.getEntity() != null) 376 { 377 file.getParentFile().mkdirs(); 378 379 try (FileOutputStream fos = new FileOutputStream(file);) 380 { 381 cmsResponse.getEntity().writeTo(fos); 382 } 383 catch (IOException e) 384 { 385 throw new IOException("Error writing the file '" + file.getAbsolutePath() + "' into the cache, check that the directory is writable.", e); 386 } 387 } 388 389 if (getLogger().isDebugEnabled()) 390 { 391 getLogger().debug("Page " + decodedPage + " written into the cache."); 392 } 393 } 394 395 /** 396 * Get the cache file to write for the corresponding page. 397 * @param root the cache root folder. 398 * @param pagePath the page path. 399 * @return a valid file. 400 */ 401 protected File getFile(File root, String pagePath) 402 { 403 File file = new File(root, pagePath); 404 405 if (!SiteCacheHelper.isValid(file)) 406 { 407 String validPath = SiteCacheHelper.getHashedFilePath(pagePath); 408 409 file = new File(root, validPath); 410 } 411 412 return file; 413 } 414 415}