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.io.UnsupportedEncodingException; 022import java.net.HttpCookie; 023import java.net.URI; 024import java.net.URLEncoder; 025import java.util.Enumeration; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.avalon.framework.parameters.Parameters; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.thread.ThreadSafe; 033import org.apache.cocoon.ProcessingException; 034import org.apache.cocoon.ResourceNotFoundException; 035import org.apache.cocoon.acting.ServiceableAction; 036import org.apache.cocoon.environment.Cookie; 037import org.apache.cocoon.environment.ObjectModelHelper; 038import org.apache.cocoon.environment.PermanentRedirector; 039import org.apache.cocoon.environment.Redirector; 040import org.apache.cocoon.environment.Request; 041import org.apache.cocoon.environment.Response; 042import org.apache.commons.lang.StringUtils; 043import org.apache.http.Header; 044import org.apache.http.HttpResponse; 045import org.apache.http.client.methods.HttpUriRequest; 046import org.apache.http.impl.client.CloseableHttpClient; 047 048import org.ametys.core.user.UserIdentity; 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 String decodedPage = new URI(page).getPath(); 122 _log("Generating the resource " + page); 123 124 Site site = (Site) request.getAttribute("site"); 125 if (site != null) 126 { 127 _getCacheAccessCounter().increaseAskedResources(site.getName()); 128 } 129 130 CloseableHttpClient httpClient = null; 131 try 132 { 133 // If the request was previously done (to test cachability), get it back 134 httpClient = (CloseableHttpClient) request.getAttribute("http-client"); 135 HttpUriRequest cmsRequest = (HttpUriRequest) request.getAttribute("cms-request"); 136 HttpResponse cmsResponse = (HttpResponse) request.getAttribute("cms-response"); 137 138 // If page was already known as non cacheable, we should do the request by now 139 if (cmsResponse == null) 140 { 141 httpClient = BackOfficeRequestHelper.getHttpClient(); 142 cmsRequest = BackOfficeRequestHelper.getRequest(objectModel, page, _requestHeaderEP); 143 cmsResponse = httpClient.execute(cmsRequest); 144 145 request.setAttribute("cms-request", cmsRequest); 146 request.setAttribute("cms-response", cmsResponse); 147 request.setAttribute("http-client", httpClient); 148 } 149 150 // Copy the Set-Cookie headers in the response. 151 _copyHeaders(request, response, cmsResponse, new String[]{"Content-Disposition", "Content-Length", "Cache-Control", "Allow", "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials", "ETag", "Last-Modified"}); 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.getStatusLine().getStatusCode(); 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(decodedPage, 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 207: 182 case 304: 183 ((org.apache.cocoon.environment.http.HttpResponse) response).setStatus(statusCode); 184 185 // Set httpClient to null because we don't want it to be shut down in the finally statement, but by the CMSResponseReader to follow 186 httpClient = null; 187 return EMPTY_MAP; 188 189 case 301: // Permanent redirection 190 _redirect(cmsResponse, redirector, page, true); 191 return null; 192 193 case 302: // Redirection 194 _redirect(cmsResponse, redirector, page, false); 195 return null; 196 197 case 401: // authorization required 198 if (site == null) 199 { 200 throw new IllegalStateException("Cannot authenticate outsite a site"); 201 } 202 SiteUrl siteUrl = site.getSiteUrls().get(0); 203 204 redirector.redirect(false, siteUrl.getBaseServerPath(request) + siteUrl.getServerPath() + "/_authenticate?requestedURL=" + _encodeRequestedUrl(request)); 205 _log("Resource '" + page + "' needs authentication"); 206 return null; 207 208 case 403: // access denied 209 UserIdentity user = site != null ? FrontAuthenticateAction.getUserIdentityFromSession(request, site.getName()) : null; 210 String userStr = user != null ? "user " + user.toString() : " anonymous user"; 211 212 _log("Access denied for resource '" + page + "' for " + userStr); 213 throw new AccessDeniedException("Access denied for " + userStr + " for URL " + cmsRequest.getURI()); 214 215 case 404: // not found 216 _log("Resource not found '" + page + "'"); 217 throw new ResourceNotFoundException("Resource not found for URL " + cmsRequest.getURI()); 218 219 case 503: // Incomplete configuration or site down. 220 _log("Site down for URL '" + page + "'"); 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) throws UnsupportedEncodingException 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 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(URLEncoder.encode(request.getParameter(parameterName), "UTF-8")); 263 } 264 if (!first) 265 { 266 requestedURI += URLEncoder.encode(transmittedParameters.toString(), "UTF-8"); // request.getAttribute("requestedURI") is already encoded 267 } 268 269 return requestedURI; 270 } 271 272 private void _log(String message) 273 { 274 if (getLogger().isDebugEnabled()) 275 { 276 getLogger().debug(message); 277 } 278 } 279 280 private void _redirect(HttpResponse cmsResponse, Redirector redirector, String page, boolean permanent) throws Exception 281 { 282 String location = cmsResponse.getFirstHeader("Location").getValue(); 283 284 if (permanent && redirector instanceof PermanentRedirector) 285 { 286 ((PermanentRedirector) redirector).permanentRedirect(false, location); 287 } 288 else 289 { 290 redirector.redirect(false, location); 291 } 292 293 _log("Redirect '" + page + "' to '" + location + "'"); 294 } 295 296 /** 297 * Copy some response headers from the back-office. 298 * @param request the front-office client request. 299 * @param response the front-office client response. 300 * @param cmsResponse the response from the back-office. 301 * @param names the header names. 302 */ 303 protected void _copyHeaders(Request request, Response response, HttpResponse cmsResponse, String[] names) 304 { 305 _transposeCookies(request, response, cmsResponse); 306 307 for (String name : names) 308 { 309 Header[] headers = cmsResponse.getHeaders(name); 310 for (Header header : headers) 311 { 312 String value = header.getValue(); 313 314 response.addHeader(name, value); 315 } 316 } 317 } 318 319 private void _transposeCookies(Request request, Response response, HttpResponse cmsResponse) 320 { 321 Header[] headers = cmsResponse.getHeaders("Set-Cookie"); 322 for (Header header : headers) 323 { 324 String value = header.getValue(); 325 326 List<HttpCookie> cookies = HttpCookie.parse(value); 327 for (HttpCookie cookie : cookies) 328 { 329 String cookieName = cookie.getName(); 330 331 if ("JSESSIONID".equals(cookieName)) 332 { 333 if (getLogger().isWarnEnabled()) 334 { 335 getLogger().warn("Receiving JSESSIONID cookie from the back-office."); 336 } 337 cookieName = __BACKOFFICE_JSESSION_ID; 338 } 339 340 Cookie newCookie = response.createCookie(cookieName, cookie.getValue()); 341 javax.servlet.http.Cookie newHttpCookie = ((org.apache.cocoon.environment.http.HttpCookie) newCookie).getServletCookie(); 342 newHttpCookie.setComment(cookie.getComment()); 343 if (cookie.getDomain() != null) 344 { 345 newHttpCookie.setDomain(cookie.getDomain()); 346 } 347 newHttpCookie.setMaxAge((int) cookie.getMaxAge()); 348 newHttpCookie.setPath(StringUtils.defaultIfEmpty(request.getContextPath(), "/")); 349 newHttpCookie.setSecure(request.isSecure()); 350 newHttpCookie.setHttpOnly(cookie.isHttpOnly()); 351 response.addCookie(newCookie); 352 } 353 } 354 } 355 356 /** 357 * Read the page from the back-office response and write it into the cache. 358 * @param decodedPage the page path. 359 * @param cmsResponse the back-office response, containing the page. 360 * @throws IOException if an error occurs writing the page into the cache. 361 */ 362 protected void _writePageOnDisk(String decodedPage, HttpResponse cmsResponse) throws IOException 363 { 364 if (getLogger().isDebugEnabled()) 365 { 366 getLogger().debug("The page is cacheable, writing into the cache: " + decodedPage); 367 } 368 369 File root = SiteCacheHelper.getRootCache(); 370 371 File file = getFile(root, decodedPage); 372 if (!file.exists() && cmsResponse != null && cmsResponse.getEntity() != null) 373 { 374 file.getParentFile().mkdirs(); 375 376 try (FileOutputStream fos = new FileOutputStream(file);) 377 { 378 cmsResponse.getEntity().writeTo(fos); 379 } 380 catch (IOException e) 381 { 382 throw new IOException("Error writing the file '" + file.getAbsolutePath() + "' into the cache, check that the directory is writable.", e); 383 } 384 } 385 386 if (getLogger().isDebugEnabled()) 387 { 388 getLogger().debug("Page " + decodedPage + " written into the cache."); 389 } 390 } 391 392 /** 393 * Get the cache file to write for the corresponding page. 394 * @param root the cache root folder. 395 * @param pagePath the page path. 396 * @return a valid file. 397 */ 398 protected File getFile(File root, String pagePath) 399 { 400 File file = new File(root, pagePath); 401 402 if (!SiteCacheHelper.isValid(file)) 403 { 404 String validPath = SiteCacheHelper.getHashedFilePath(pagePath); 405 406 file = new File(root, validPath); 407 } 408 409 return file; 410 } 411 412}