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.URI; 023import java.net.URLEncoder; 024import java.util.Enumeration; 025import java.util.Map; 026import java.util.regex.Matcher; 027import java.util.regex.Pattern; 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.ObjectModelHelper; 037import org.apache.cocoon.environment.PermanentRedirector; 038import org.apache.cocoon.environment.Redirector; 039import org.apache.cocoon.environment.Request; 040import org.apache.cocoon.environment.Response; 041import org.apache.commons.lang.StringUtils; 042import org.apache.http.Header; 043import org.apache.http.HttpResponse; 044import org.apache.http.client.methods.HttpUriRequest; 045import org.apache.http.impl.client.CloseableHttpClient; 046 047import org.ametys.core.user.UserIdentity; 048import org.ametys.plugins.site.Site; 049import org.ametys.plugins.site.SiteUrl; 050import org.ametys.plugins.site.headers.RequestHeaderExtensionPoint; 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 static final Pattern __COOKIE_PATH_PATTERN = Pattern.compile(".*(Path=[^;]+).*"); 062 063 private CacheAccessManager _cacheAccess; 064 private CacheAccessCounter _cacheAccessCounter; 065 private RequestHeaderExtensionPoint _requestHeaderEP; 066 067 @Override 068 public void service(ServiceManager sManager) throws ServiceException 069 { 070 super.service(sManager); 071 _cacheAccess = (CacheAccessManager) sManager.lookup(CacheAccessManager.ROLE); 072 _cacheAccessCounter = (CacheAccessCounter) sManager.lookup(CacheAccessCounter.ROLE); 073 _requestHeaderEP = (RequestHeaderExtensionPoint) sManager.lookup(RequestHeaderExtensionPoint.ROLE); 074 } 075 076 @Override 077 public Map act(Redirector redirector, org.apache.cocoon.environment.SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception 078 { 079 Request request = ObjectModelHelper.getRequest(objectModel); 080 Response response = ObjectModelHelper.getResponse(objectModel); 081 082 String page = parameters.getParameter("page"); 083 _log("Generating the resource " + page); 084 085 Site site = (Site) request.getAttribute("site"); 086 if (site != null) 087 { 088 _cacheAccessCounter.increaseAskedResources(site.getName()); 089 } 090 091 CloseableHttpClient httpClient = null; 092 try 093 { 094 // If the request was previously done (to test cachability), get it back 095 httpClient = (CloseableHttpClient) request.getAttribute("http-client"); 096 HttpUriRequest cmsRequest = (HttpUriRequest) request.getAttribute("cms-request"); 097 HttpResponse cmsResponse = (HttpResponse) request.getAttribute("cms-response"); 098 099 // If page was already known as non cacheable, we should do the request by now 100 if (cmsResponse == null) 101 { 102 httpClient = BackOfficeRequestHelper.getHttpClient(); 103 cmsRequest = BackOfficeRequestHelper.getRequest(objectModel, page, _requestHeaderEP); 104 cmsResponse = httpClient.execute(cmsRequest); 105 106 request.setAttribute("cms-request", cmsRequest); 107 request.setAttribute("cms-response", cmsResponse); 108 request.setAttribute("http-client", httpClient); 109 } 110 111 // Copy the Set-Cookie headers in the response. 112 _copyHeaders(request, response, cmsResponse, new String[]{"Set-Cookie", "Set-Cookie2", "Content-Disposition", "Cache-Control", "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials"}); 113 114 // Handle the request 115 int statusCode = cmsResponse.getStatusLine().getStatusCode(); 116 switch (statusCode) 117 { 118 case 200: 119 Header header = cmsResponse.getFirstHeader("X-Ametys-Cacheable"); 120 if (header != null && "true".equals(header.getValue())) 121 { 122 String decodedPage = new URI(page).getPath(); 123 _writePageOnDisk(decodedPage, cmsResponse); 124 125 _log("Succeed to generate cacheable resource '" + page + "'"); 126 127 return null; 128 } 129 else 130 { 131 // Set httpClient to null because we don't want it to be shut down in the finally statement, but by the CMSResponseReader to follow 132 httpClient = null; 133 _log("Succeed to generate uncacheable resource '" + page + "'"); 134 135 return EMPTY_MAP; 136 } 137 case 301: // Permanent redirection 138 _redirect(cmsResponse, redirector, page, true); 139 return null; 140 141 case 302: // Redirection 142 _redirect(cmsResponse, redirector, page, false); 143 return null; 144 145 case 401: // authorization required 146 if (site == null) 147 { 148 throw new IllegalStateException("Cannot authenticate outsite a site"); 149 } 150 SiteUrl siteUrl = site.getSiteUrls().get(0); 151 152 redirector.redirect(false, siteUrl.getBaseServerPath(request) + siteUrl.getServerPath() + "/_authenticate?requestedURL=" + _encodeRequestedUrl(request)); 153 _log("Resource '" + page + "' needs authentication"); 154 return null; 155 156 case 403: // access denied 157 UserIdentity user = site != null ? FrontAuthenticateAction.getUserIdentityFromSession(request, site.getName()) : null; 158 String userStr = user != null ? "user " + user.toString() : " anonymous user"; 159 160 _log("Access denied for resource '" + page + "' for " + userStr); 161 throw new AccessDeniedException("Access denied for " + userStr + " for URL " + cmsRequest.getURI()); 162 163 case 404: // not found 164 _log("Resource not found '" + page + "'"); 165 throw new ResourceNotFoundException("Resource not found for URL " + cmsRequest.getURI()); 166 167 case 503: // Incomplete configuration or site down. 168 _log("Site down for URL '" + page + "'"); 169 throw new ServiceUnavailableException("Site down for URL " + cmsRequest.getURI()); 170 171 default: 172 _log("Unable to get resource '" + page + "'. Status code is " + statusCode); 173 throw new ProcessingException("Unable to get URL '" + page + "' at URL '" + cmsRequest.getURI() + "'. Status code is " + statusCode); 174 } 175 } 176 finally 177 { 178 // Whatever happens, unlock the page. 179 _cacheAccess.unlock(page); 180 181 if (httpClient != null) 182 { 183 httpClient.close(); 184 } 185 } 186 } 187 188 private String _encodeRequestedUrl(Request request) throws UnsupportedEncodingException 189 { 190 String requestedURI = (String) request.getAttribute("requestedURI"); 191 192 // Transmit parameters 193 StringBuilder transmittedParameters = new StringBuilder(); 194 boolean first = true; 195 Enumeration<String> parameterNames = request.getParameterNames(); 196 while (parameterNames.hasMoreElements()) 197 { 198 if (first) 199 { 200 transmittedParameters.append("?"); 201 first = false; 202 } 203 else 204 { 205 transmittedParameters.append("&"); 206 } 207 String parameterName = parameterNames.nextElement(); 208 transmittedParameters.append(parameterName); 209 transmittedParameters.append("="); 210 transmittedParameters.append(URLEncoder.encode(request.getParameter(parameterName), "UTF-8")); 211 } 212 if (!first) 213 { 214 requestedURI += URLEncoder.encode(transmittedParameters.toString(), "UTF-8"); // request.getAttribute("requestedURI") is already encoded 215 } 216 217 return requestedURI; 218 } 219 220 private void _log(String message) 221 { 222 if (getLogger().isDebugEnabled()) 223 { 224 getLogger().debug(message); 225 } 226 } 227 228 private void _redirect(HttpResponse cmsResponse, Redirector redirector, String page, boolean permanent) throws Exception 229 { 230 String location = cmsResponse.getFirstHeader("Location").getValue(); 231 232 if (permanent && redirector instanceof PermanentRedirector) 233 { 234 ((PermanentRedirector) redirector).permanentRedirect(false, location); 235 } 236 else 237 { 238 redirector.redirect(false, location); 239 } 240 241 _log("Redirect '" + page + "' to '" + location + "'"); 242 } 243 244 /** 245 * Copy some response headers from the back-office. 246 * @param request the front-office client request. 247 * @param response the front-office client response. 248 * @param cmsResponse the response from the back-office. 249 * @param names the header names. 250 */ 251 protected void _copyHeaders(Request request, Response response, HttpResponse cmsResponse, String[] names) 252 { 253 for (String name : names) 254 { 255 Header[] headers = cmsResponse.getHeaders(name); 256 for (Header header : headers) 257 { 258 String value = header.getValue(); 259 260 if (name.startsWith("Set-Cookie") && value.startsWith("JSESSIONID=")) 261 { 262 if (getLogger().isWarnEnabled()) 263 { 264 getLogger().warn("Receiving JSESSIONID cookie from the back-office."); 265 } 266 267 Matcher cookieMatcher = __COOKIE_PATH_PATTERN.matcher(value); 268 if (cookieMatcher.matches()) 269 { 270 String path = cookieMatcher.group(1); 271 String newPath = request.getContextPath(); 272 if (StringUtils.isEmpty(newPath)) 273 { 274 newPath = "/"; 275 } 276 value = value.replace(path, "Path=" + newPath); 277 } 278 value = value.replace("JSESSIONID=", __BACKOFFICE_JSESSION_ID + "="); 279 280 } 281 282 response.addHeader(name, value); 283 } 284 } 285 } 286 287 /** 288 * Read the page from the back-office response and write it into the cache. 289 * @param decodedPage the page path. 290 * @param cmsResponse the back-office response, containing the page. 291 * @throws IOException if an error occurs writing the page into the cache. 292 */ 293 protected void _writePageOnDisk(String decodedPage, HttpResponse cmsResponse) throws IOException 294 { 295 if (getLogger().isDebugEnabled()) 296 { 297 getLogger().debug("The page is cacheable, writing into the cache: " + decodedPage); 298 } 299 300 File root = SiteCacheHelper.getRootCache(); 301 302 File file = getFile(root, decodedPage); 303 if (!file.exists() && cmsResponse != null && cmsResponse.getEntity() != null) 304 { 305 file.getParentFile().mkdirs(); 306 307 try (FileOutputStream fos = new FileOutputStream(file);) 308 { 309 cmsResponse.getEntity().writeTo(fos); 310 } 311 catch (IOException e) 312 { 313 throw new IOException("Error writing the file '" + file.getAbsolutePath() + "' into the cache, check that the directory is writable.", e); 314 } 315 } 316 317 if (getLogger().isDebugEnabled()) 318 { 319 getLogger().debug("Page " + decodedPage + " written into the cache."); 320 } 321 } 322 323 /** 324 * Get the cache file to write for the corresponding page. 325 * @param root the cache root folder. 326 * @param pagePath the page path. 327 * @return a valid file. 328 */ 329 protected File getFile(File root, String pagePath) 330 { 331 File file = new File(root, pagePath); 332 333 if (!SiteCacheHelper.isValid(file)) 334 { 335 String validPath = SiteCacheHelper.getHashedFilePath(pagePath); 336 337 file = new File(root, validPath); 338 } 339 340 return file; 341 } 342 343}