001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.main; 029 030import org.opencms.configuration.CmsParameterConfiguration; 031import org.opencms.configuration.I_CmsConfigurationParameterHandler; 032import org.opencms.configuration.I_CmsNeedsAdminCmsObject; 033import org.opencms.file.CmsObject; 034import org.opencms.file.CmsResource; 035import org.opencms.file.CmsResourceFilter; 036import org.opencms.file.CmsVfsResourceNotFoundException; 037import org.opencms.file.types.I_CmsResourceType; 038import org.opencms.relations.CmsLink; 039import org.opencms.relations.I_CmsCustomLinkRenderer; 040import org.opencms.security.CmsPermissionViolationException; 041import org.opencms.util.CmsFileUtil; 042import org.opencms.util.CmsStringUtil; 043import org.opencms.xml.xml2json.I_CmsApiAuthorizationHandler; 044 045import java.io.IOException; 046import java.net.URI; 047import java.net.URISyntaxException; 048import java.util.List; 049import java.util.regex.Pattern; 050 051import javax.servlet.http.HttpServletRequest; 052import javax.servlet.http.HttpServletResponse; 053 054import org.apache.commons.logging.Log; 055import org.apache.http.NameValuePair; 056import org.apache.http.client.utils.URIBuilder; 057 058/** 059 * Resource init handler that provides an alternative way of serving static files like images or binary files, using the API authorization mechanism 060 * instead of the normal authorization handler. 061 * 062 * <p>Resources are accessed by appending their VFS root path to the /staticresource handler path. When resources are requested this way, they are still 063 * loaded with the normal OpenCms loader mechanism. This works for the intended use case (binary files, images) but may not work for other types. 064 * 065 * <p>The resources accessible through this handler can be restricted by setting regex configuration parameters for path and type which the requested resources 066 * have to match. 067 * 068 * <p>This can be used in combination with the CmsJsonResourceHandler class. When configured correctly (using the linkrewrite.id parameter on this handler, 069 * and a matching linkrewrite.refid on the CmsJsonResourceHandler), links to resources this handler is responsible for will be rewritten to point to the URL 070 * for the resource using this handler. 071 */ 072public class CmsProtectedStaticFileHandler 073implements I_CmsResourceInit, I_CmsConfigurationParameterHandler, I_CmsNeedsAdminCmsObject, I_CmsCustomLinkRenderer { 074 075 /** Parameter for defining the id under which the link renderer should be registered. */ 076 public static final String PARAM_LINKREWRITE_ID = "linkrewrite.id"; 077 078 /** Configuration parameter that determines which authorization method to use. */ 079 public static final String PARAM_AUTHORIZATION = "authorization"; 080 081 /** Configuration parameter for the path filter regex. */ 082 public static final String PARAM_PATHFILTER = "pathfilter"; 083 084 /** Configuration parameter for the type filter regex. */ 085 public static final String PARAM_TYPEFILTER = "typefilter"; 086 087 /** URL prefix. */ 088 public static final String PREFIX = "/staticresource"; 089 090 /** Logger instance for this class. */ 091 private static final Log LOG = CmsLog.getLog(CmsProtectedStaticFileHandler.class); 092 093 public static final String PARAM_LINKREWRITE_PREFIX = "linkrewrite.prefix"; 094 095 /** The Admin CMS context. */ 096 private CmsObject m_adminCms; 097 098 /** Configuration from config file. */ 099 private CmsParameterConfiguration m_config = new CmsParameterConfiguration(); 100 101 /** Regex for matching paths. */ 102 private Pattern m_pathFilter; 103 104 /** Regex for matching types. */ 105 private Pattern m_typeFilter; 106 107 /** The link rewrite prefix. */ 108 private String m_linkRewritePrefix; 109 110 /** 111 * Merges a link prefix with additional link components. 112 * 113 * @param prefix the prefix 114 * @param path the path 115 * @param query the query 116 * 117 * @return the combined link 118 */ 119 public static String mergeLinkPrefix(String prefix, String path, String query) { 120 121 try { 122 URI baseUri = new URI(prefix); 123 124 // we can't give an URIBuilder an already escaped query string, so we parse a dummy URL with the query string 125 // and use its parameter list for constructing the final URI 126 URI queryStringUri = new URI("http://test.invalid" + (query != null ? ("?" + query) : "")); 127 List<NameValuePair> params = new URIBuilder(queryStringUri).getQueryParams(); 128 String result = new URIBuilder(baseUri).setPath( 129 CmsStringUtil.joinPaths(baseUri.getPath(), PREFIX, path)).setParameters(params).build().toASCIIString(); 130 return result; 131 } catch (URISyntaxException e) { 132 LOG.error(e.getLocalizedMessage(), e); 133 return null; 134 } 135 } 136 137 /** 138 * Helper method for authorizing requests based on a comma-separated list of API authorization handler names. 139 * 140 * <p>This will evaluate each authorization handler from authChain and return the first non-null CmsObject returned. 141 * A special case is authChain contains the word 'default', this is not u 142 * 143 * <p>Returns null if the authorization failed. 144 * 145 * @param adminCms the Admin CmsObject 146 * @param defaultCms the current CmsObject with the default user data from the request 147 * @param request the current request 148 * @param authChain a comma-separated list of API authorization handler names 149 * 150 * @return the initialized CmsObject 151 */ 152 private static CmsObject authorize( 153 CmsObject adminCms, 154 CmsObject defaultCms, 155 HttpServletRequest request, 156 String authChain) { 157 158 if (authChain == null) { 159 return defaultCms; 160 } 161 for (String token : authChain.split(",")) { 162 token = token.trim(); 163 if ("default".equals(token)) { 164 LOG.info("Using default CmsObject"); 165 return defaultCms; 166 } else if ("guest".equals(token)) { 167 try { 168 return OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest()); 169 } catch (CmsException e) { 170 LOG.error(e.getLocalizedMessage(), e); 171 return null; 172 } 173 } else { 174 I_CmsApiAuthorizationHandler handler = OpenCms.getApiAuthorization(token); 175 if (handler == null) { 176 LOG.error("Could not find API authorization handler " + token); 177 return null; 178 } else { 179 try { 180 CmsObject cms = handler.initCmsObject(adminCms, request); 181 if (cms != null) { 182 LOG.info("Succeeded with authorization handler: " + token); 183 return cms; 184 } 185 } catch (CmsException e) { 186 LOG.error("Error evaluating authorization handler " + token); 187 return null; 188 } 189 } 190 } 191 } 192 LOG.info("Authentication unsusccessful"); 193 return null; 194 } 195 196 /** 197 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String) 198 */ 199 public void addConfigurationParameter(String paramName, String paramValue) { 200 201 m_config.add(paramName, paramValue); 202 } 203 204 /** 205 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration() 206 */ 207 public CmsParameterConfiguration getConfiguration() { 208 209 return m_config; 210 } 211 212 /** 213 * @see org.opencms.relations.I_CmsCustomLinkRenderer#getLink(org.opencms.file.CmsObject, org.opencms.relations.CmsLink) 214 */ 215 public String getLink(CmsObject cms, CmsLink link) { 216 217 try { 218 CmsObject adminCms = OpenCms.initCmsObject(m_adminCms); 219 adminCms.getRequestContext().setCurrentProject(cms.getRequestContext().getCurrentProject()); 220 link.checkConsistency(adminCms); 221 222 if (checkResourceAccessible(link.getResource())) { 223 return mergeLinkPrefix(m_linkRewritePrefix, link.getResource().getRootPath(), link.getQuery()); 224 } 225 return null; 226 } catch (CmsException e) { 227 LOG.warn(e.getLocalizedMessage(), e); 228 return null; 229 } 230 } 231 232 /** 233 * @see org.opencms.relations.I_CmsCustomLinkRenderer#getLink(org.opencms.file.CmsObject, org.opencms.file.CmsResource) 234 */ 235 public String getLink(CmsObject cms, CmsResource resource) { 236 237 if (checkResourceAccessible(resource)) { 238 return mergeLinkPrefix(m_linkRewritePrefix, resource.getRootPath(), null); 239 } 240 return null; 241 } 242 243 /** 244 * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration() 245 */ 246 public void initConfiguration() { 247 248 m_config = CmsParameterConfiguration.unmodifiableVersion(m_config); 249 m_pathFilter = Pattern.compile(m_config.getString(PARAM_PATHFILTER, ".*")); 250 m_typeFilter = Pattern.compile(m_config.getString(PARAM_TYPEFILTER, "image|text|binary")); 251 String linkRewriteId = m_config.getString(PARAM_LINKREWRITE_ID, null); 252 if (linkRewriteId != null) { 253 OpenCms.setRuntimeProperty(linkRewriteId, this); 254 } 255 m_linkRewritePrefix = m_config.getString(PARAM_LINKREWRITE_PREFIX, null); 256 } 257 258 /** 259 * @see org.opencms.main.I_CmsResourceInit#initResource(org.opencms.file.CmsResource, org.opencms.file.CmsObject, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) 260 */ 261 public CmsResource initResource(CmsResource origRes, CmsObject cms, HttpServletRequest req, HttpServletResponse res) 262 throws CmsResourceInitException { 263 264 String uri = cms.getRequestContext().getUri(); 265 266 if (origRes != null) { 267 return origRes; 268 } 269 if (res == null) { 270 // called from locale handler 271 return origRes; 272 } 273 if (!CmsStringUtil.isPrefixPath(PREFIX, uri)) { 274 return null; 275 } 276 String path = uri.substring(PREFIX.length()); 277 if (path.isEmpty()) { 278 path = "/"; 279 } else if (path.length() > 1) { 280 path = CmsFileUtil.removeTrailingSeparator(path); 281 } 282 283 String authorizationParam = m_config.get(PARAM_AUTHORIZATION); 284 CmsObject origCms = cms; 285 cms = authorize(m_adminCms, origCms, req, authorizationParam); 286 if ((cms != null) && (cms != origCms)) { 287 origCms.getRequestContext().setAttribute(I_CmsResourceInit.ATTR_ALTERNATIVE_CMS_OBJECT, cms); 288 cms.getRequestContext().setSiteRoot(origCms.getRequestContext().getSiteRoot()); 289 cms.getRequestContext().setUri(origCms.getRequestContext().getUri()); 290 } 291 int status = 200; 292 try { 293 CmsObject rootCms = OpenCms.initCmsObject(cms); 294 rootCms.getRequestContext().setSiteRoot(""); 295 if (m_pathFilter.matcher(path).matches()) { 296 CmsResource resource = rootCms.readResource(path, CmsResourceFilter.IGNORE_EXPIRATION); 297 if (!checkResourceAccessible(resource)) { 298 status = HttpServletResponse.SC_FORBIDDEN; 299 } else { 300 return resource; 301 } 302 } 303 status = HttpServletResponse.SC_NOT_FOUND; 304 } catch (CmsPermissionViolationException e) { 305 if (OpenCms.getDefaultUsers().isUserGuest(cms.getRequestContext().getCurrentUser().getName())) { 306 status = HttpServletResponse.SC_UNAUTHORIZED; 307 } else { 308 status = HttpServletResponse.SC_FORBIDDEN; 309 } 310 } catch (CmsVfsResourceNotFoundException e) { 311 status = HttpServletResponse.SC_NOT_FOUND; 312 } catch (CmsException e) { 313 LOG.error(e.getLocalizedMessage(), e); 314 status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; 315 } 316 try { 317 res.sendError(status); 318 } catch (IOException e) { 319 LOG.error(e.getLocalizedMessage(), e); 320 } 321 CmsResourceInitException ex = new CmsResourceInitException(CmsProtectedStaticFileHandler.class); 322 ex.setClearErrors(true); 323 throw ex; 324 } 325 326 /** 327 * @see org.opencms.configuration.I_CmsNeedsAdminCmsObject#setAdminCmsObject(org.opencms.file.CmsObject) 328 */ 329 public void setAdminCmsObject(CmsObject adminCms) { 330 331 m_adminCms = adminCms; 332 333 } 334 335 /** 336 * Checks if the resource is not hidden according to the filters configured in the resource handler parameters. 337 * 338 * @param res the resource to check 339 * @return true if the resource is accessible 340 */ 341 private boolean checkResourceAccessible(CmsResource res) { 342 343 return (res != null) && m_pathFilter.matcher(res.getRootPath()).matches() && checkType(res.getTypeId()); 344 } 345 346 /** 347 * Checks that the type matches the configured type filter 348 * 349 * @param typeId a type id 350 * @return true if the type matches the configured type filter 351 */ 352 private boolean checkType(int typeId) { 353 354 try { 355 I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(typeId); 356 return m_typeFilter.matcher(type.getTypeName()).matches(); 357 } catch (Exception e) { 358 LOG.error("Missing type with id: " + typeId); 359 return false; 360 } 361 362 } 363 364}