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