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}