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.regex.Pattern;
049
050import javax.servlet.http.HttpServletRequest;
051import javax.servlet.http.HttpServletResponse;
052
053import org.apache.commons.logging.Log;
054
055/**
056 * Resource init handler that provides an alternative way of serving static files like images or binary files, using the API authorization mechanism
057 * instead of the normal authorization handler.
058 *
059 * <p>Resources are accessed by appending their VFS root path to the /staticresource handler path. When resources are requested this way, they are still
060 * loaded with the normal OpenCms loader mechanism. This works for the intended use case (binary files, images) but may not work for other types.
061 *
062 * <p>The resources accessible through this handler can be restricted by setting regex configuration parameters for path and type which the requested resources
063 * have to match.
064 *
065 * <p>This can be used in combination with the CmsJsonResourceHandler class. When configured correctly (using the linkrewrite.id parameter on this handler,
066 * and a matching linkrewrite.refid on the CmsJsonResourceHandler), links to resources this handler is responsible for will be rewritten to point to the URL
067 * for the resource using this handler.
068 */
069public class CmsProtectedStaticFileHandler
070implements I_CmsResourceInit, I_CmsConfigurationParameterHandler, I_CmsNeedsAdminCmsObject, I_CmsCustomLinkRenderer {
071
072    /** Parameter for defining the id under which the link renderer should be registered. */
073    public static final String PARAM_LINKREWRITE_ID = "linkrewrite.id";
074
075    /** Configuration parameter that determines which authorization method to use. */
076    public static final String PARAM_AUTHORIZATION = "authorization";
077
078    /** Configuration parameter for the path filter regex. */
079    public static final String PARAM_PATHFILTER = "pathfilter";
080
081    /** Configuration parameter for the type filter regex. */
082    public static final String PARAM_TYPEFILTER = "typefilter";
083
084    /** URL prefix. */
085    public static final String PREFIX = "/staticresource";
086
087    /** Logger instance for this class. */
088    private static final Log LOG = CmsLog.getLog(CmsProtectedStaticFileHandler.class);
089
090    public static final String PARAM_LINKREWRITE_PREFIX = "linkrewrite.prefix";
091
092    /** The Admin CMS context. */
093    private CmsObject m_adminCms;
094
095    /** Configuration from config file. */
096    private CmsParameterConfiguration m_config = new CmsParameterConfiguration();
097
098    /** Regex for matching paths. */
099    private Pattern m_pathFilter;
100
101    /** Regex for matching types. */
102    private Pattern m_typeFilter;
103
104    /** The link rewrite prefix. */
105    private String m_linkRewritePrefix;
106
107    /**
108     * Merges a link prefix with additional link components.
109     *
110     * @param prefix the prefix
111     * @param path the path
112     * @param query the query
113     *
114     * @return the combined link
115     */
116    public static String mergeLinkPrefix(String prefix, String path, String query) {
117
118        try {
119            URI baseUri = new URI(prefix);
120            URI correctedUri = new URI(
121                baseUri.getScheme(),
122                baseUri.getAuthority(),
123                CmsStringUtil.joinPaths(baseUri.getPath(), PREFIX, path),
124                query,
125                null);
126            return correctedUri.toASCIIString();
127        } catch (URISyntaxException e) {
128            LOG.error(e.getLocalizedMessage(), e);
129            return null;
130        }
131
132    }
133
134    /**
135     * Helper method for authorizing requests based on a comma-separated list of API authorization handler names.
136     *
137     * <p>This will evaluate each authorization handler from authChain and return the first non-null CmsObject returned.
138     * A special case is authChain contains the word 'default', this is not u
139     *
140     * <p>Returns null if the authorization failed.
141     *
142     * @param adminCms the Admin CmsObject
143     * @param defaultCms the current CmsObject with the default user data from the request
144     * @param request the current request
145     * @param authChain a comma-separated list of API authorization handler names
146     *
147     * @return the initialized CmsObject
148     */
149    private static CmsObject authorize(
150        CmsObject adminCms,
151        CmsObject defaultCms,
152        HttpServletRequest request,
153        String authChain) {
154
155        if (authChain == null) {
156            return defaultCms;
157        }
158        for (String token : authChain.split(",")) {
159            token = token.trim();
160            if ("default".equals(token)) {
161                LOG.info("Using default CmsObject");
162                return defaultCms;
163            } else if ("guest".equals(token)) {
164                try {
165                    return OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest());
166                } catch (CmsException e) {
167                    LOG.error(e.getLocalizedMessage(), e);
168                    return null;
169                }
170            } else {
171                I_CmsApiAuthorizationHandler handler = OpenCms.getApiAuthorization(token);
172                if (handler == null) {
173                    LOG.error("Could not find API authorization handler " + token);
174                    return null;
175                } else {
176                    try {
177                        CmsObject cms = handler.initCmsObject(adminCms, request);
178                        if (cms != null) {
179                            LOG.info("Succeeded with authorization handler: " + token);
180                            return cms;
181                        }
182                    } catch (CmsException e) {
183                        LOG.error("Error evaluating authorization handler " + token);
184                        return null;
185                    }
186                }
187            }
188        }
189        LOG.info("Authentication unsusccessful");
190        return null;
191    }
192
193    /**
194     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#addConfigurationParameter(java.lang.String, java.lang.String)
195     */
196    public void addConfigurationParameter(String paramName, String paramValue) {
197
198        m_config.add(paramName, paramValue);
199    }
200
201    /**
202     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#getConfiguration()
203     */
204    public CmsParameterConfiguration getConfiguration() {
205
206        return m_config;
207    }
208
209    /**
210     * @see org.opencms.relations.I_CmsCustomLinkRenderer#getLink(org.opencms.file.CmsObject, org.opencms.relations.CmsLink)
211     */
212    public String getLink(CmsObject cms, CmsLink link) {
213
214        try {
215            CmsObject adminCms = OpenCms.initCmsObject(m_adminCms);
216            adminCms.getRequestContext().setCurrentProject(cms.getRequestContext().getCurrentProject());
217            link.checkConsistency(adminCms);
218
219            if (checkResourceAccessible(link.getResource())) {
220                return mergeLinkPrefix(m_linkRewritePrefix, link.getResource().getRootPath(), link.getQuery());
221            }
222            return null;
223        } catch (CmsException e) {
224            LOG.warn(e.getLocalizedMessage(), e);
225            return null;
226        }
227    }
228
229    /**
230     * @see org.opencms.relations.I_CmsCustomLinkRenderer#getLink(org.opencms.file.CmsObject, org.opencms.file.CmsResource)
231     */
232    public String getLink(CmsObject cms, CmsResource resource) {
233
234        if (checkResourceAccessible(resource)) {
235            return mergeLinkPrefix(m_linkRewritePrefix, resource.getRootPath(), null);
236        }
237        return null;
238    }
239
240    /**
241     * @see org.opencms.configuration.I_CmsConfigurationParameterHandler#initConfiguration()
242     */
243    public void initConfiguration() {
244
245        m_config = CmsParameterConfiguration.unmodifiableVersion(m_config);
246        m_pathFilter = Pattern.compile(m_config.getString(PARAM_PATHFILTER, ".*"));
247        m_typeFilter = Pattern.compile(m_config.getString(PARAM_TYPEFILTER, "image|text|binary"));
248        String linkRewriteId = m_config.getString(PARAM_LINKREWRITE_ID, null);
249        if (linkRewriteId != null) {
250            OpenCms.setRuntimeProperty(linkRewriteId, this);
251        }
252        m_linkRewritePrefix = m_config.getString(PARAM_LINKREWRITE_PREFIX, null);
253    }
254
255    /**
256     * @see org.opencms.main.I_CmsResourceInit#initResource(org.opencms.file.CmsResource, org.opencms.file.CmsObject, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
257     */
258    public CmsResource initResource(CmsResource origRes, CmsObject cms, HttpServletRequest req, HttpServletResponse res)
259    throws CmsResourceInitException {
260
261        String uri = cms.getRequestContext().getUri();
262
263        if (origRes != null) {
264            return origRes;
265        }
266        if (res == null) {
267            // called from locale handler
268            return origRes;
269        }
270        if (!CmsStringUtil.isPrefixPath(PREFIX, uri)) {
271            return null;
272        }
273        String path = uri.substring(PREFIX.length());
274        if (path.isEmpty()) {
275            path = "/";
276        } else if (path.length() > 1) {
277            path = CmsFileUtil.removeTrailingSeparator(path);
278        }
279
280        String authorizationParam = m_config.get(PARAM_AUTHORIZATION);
281        CmsObject origCms = cms;
282        cms = authorize(m_adminCms, origCms, req, authorizationParam);
283        if ((cms != null) && (cms != origCms)) {
284            origCms.getRequestContext().setAttribute(I_CmsResourceInit.ATTR_ALTERNATIVE_CMS_OBJECT, cms);
285            cms.getRequestContext().setSiteRoot(origCms.getRequestContext().getSiteRoot());
286            cms.getRequestContext().setUri(origCms.getRequestContext().getUri());
287        }
288        int status = 200;
289        try {
290            CmsObject rootCms = OpenCms.initCmsObject(cms);
291            rootCms.getRequestContext().setSiteRoot("");
292            if (m_pathFilter.matcher(path).matches()) {
293                CmsResource resource = rootCms.readResource(path, CmsResourceFilter.IGNORE_EXPIRATION);
294                if (!checkResourceAccessible(resource)) {
295                    status = HttpServletResponse.SC_FORBIDDEN;
296                } else {
297                    return resource;
298                }
299            }
300            status = HttpServletResponse.SC_NOT_FOUND;
301        } catch (CmsPermissionViolationException e) {
302            if (OpenCms.getDefaultUsers().isUserGuest(cms.getRequestContext().getCurrentUser().getName())) {
303                status = HttpServletResponse.SC_UNAUTHORIZED;
304            } else {
305                status = HttpServletResponse.SC_FORBIDDEN;
306            }
307        } catch (CmsVfsResourceNotFoundException e) {
308            status = HttpServletResponse.SC_NOT_FOUND;
309        } catch (CmsException e) {
310            LOG.error(e.getLocalizedMessage(), e);
311            status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
312        }
313        try {
314            res.sendError(status);
315        } catch (IOException e) {
316            LOG.error(e.getLocalizedMessage(), e);
317        }
318        CmsResourceInitException ex = new CmsResourceInitException(CmsProtectedStaticFileHandler.class);
319        ex.setClearErrors(true);
320        throw ex;
321    }
322
323    /**
324     * @see org.opencms.configuration.I_CmsNeedsAdminCmsObject#setAdminCmsObject(org.opencms.file.CmsObject)
325     */
326    public void setAdminCmsObject(CmsObject adminCms) {
327
328        m_adminCms = adminCms;
329
330    }
331
332    /**
333     * Checks if the resource is not hidden according to the filters configured in the resource handler parameters.
334     *
335     * @param res the resource to check
336     * @return true if the resource is accessible
337     */
338    private boolean checkResourceAccessible(CmsResource res) {
339
340        return (res != null) && m_pathFilter.matcher(res.getRootPath()).matches() && checkType(res.getTypeId());
341    }
342
343    /**
344     * Checks that the type matches the configured type filter
345     *
346     * @param typeId a type id
347     * @return true if the type matches the configured type filter
348     */
349    private boolean checkType(int typeId) {
350
351        try {
352            I_CmsResourceType type = OpenCms.getResourceManager().getResourceType(typeId);
353            return m_typeFilter.matcher(type.getTypeName()).matches();
354        } catch (Exception e) {
355            LOG.error("Missing type with id: " + typeId);
356            return false;
357        }
358
359    }
360
361}