/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.fsresources; import org.opencastproject.security.api.Role; import org.opencastproject.security.api.SecurityService; import org.opencastproject.util.XProperties; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; /** * Serves static content from a configured path on the filesystem. In production systems, this should be replaced with * apache httpd or another web server optimized for serving static content. */ public class ResourceServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger logger = LoggerFactory.getLogger(ResourceServlet.class); protected String root; protected String serverAlias; protected DocumentBuilder builder = null; private static final String dateFormat = "yyyy-MM-dd HH:mm:ss Z"; private SecurityService securityService = null; public ResourceServlet() { } public ResourceServlet(String alias, String filesystemDir) { root = filesystemDir; serverAlias = alias; } public SecurityService getSecurityService() { return securityService; } public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } public void activate(ComponentContext cc) throws ParserConfigurationException { builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); XProperties props = new XProperties(); props.setBundleContext(cc.getBundleContext()); String rootKey = (String) cc.getProperties().get("rootKey"); if (rootKey != null) { if (root == null) root = (String) cc.getProperties().get(rootKey); if (root == null) { logger.warn("No value for key " + rootKey + " found for this service. Defaulting to value of org.opencastproject.download.directory."); } } if (root == null) { root = (String) cc.getBundleContext().getProperty("org.opencastproject.download.directory"); } if (root == null) { throw new IllegalStateException("Unable to find root for servlet, please check your config files."); } if (serverAlias == null) serverAlias = (String) cc.getProperties().get("alias"); // Get the interpreted values of the keys. props.put("root", root); root = props.getProperty("root"); props.put("serverAlias", serverAlias); serverAlias = props.getProperty("serverAlias"); if (serverAlias == null || StringUtils.isBlank(serverAlias)) { throw new IllegalStateException("Unable to create servlet, alias property is null"); } else if (root == null) { throw new IllegalStateException("Unable to create servlet, root property is null"); } if (serverAlias.charAt(0) != '/') { serverAlias = '/' + serverAlias; } File rootDir = new File(root); if (!rootDir.exists()) { if (!rootDir.mkdirs()) { logger.error("Unable to create directories for {}!", rootDir.getAbsolutePath()); return; } } if (!(rootDir.isDirectory() || rootDir.isFile())) { throw new IllegalStateException("Unable to create servlet for " + serverAlias + " because " + rootDir.getAbsolutePath() + " is not a file or directory!"); } logger.debug("Activating servlet with alias " + serverAlias + " on directory " + rootDir.getAbsolutePath()); } /** * {@inheritDoc} * * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { logger.debug("Looking for static resource '{}'", req.getRequestURI()); String path = req.getPathInfo(); String normalized = path == null ? "/" : path.trim().replaceAll("/+", "/").replaceAll("\\.\\.", ""); if (path == null) { path = "/"; } else { // Replace duplicate slashes with a single slash, and remove .. from the listing path = path.trim().replaceAll("/+", "/").replaceAll("\\.\\.", ""); } if (normalized != null && normalized.startsWith("/") && normalized.length() > 1) { normalized = normalized.substring(1); } File f = new File(root, normalized); boolean allowed = true; if (f.isFile() && f.canRead()) { allowed = checkDirectory(f.getParentFile()); if (!allowed) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } logger.debug("Serving static resource '{}'", f.getAbsolutePath()); FileInputStream in = new FileInputStream(f); try { IOUtils.copyLarge(in, resp.getOutputStream()); } finally { IOUtils.closeQuietly(in); } } else if (f.isDirectory() && f.canRead()) { allowed = checkDirectory(f); if (!allowed) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } logger.debug("Serving index page for '{}'", f.getAbsolutePath()); PrintWriter out = resp.getWriter(); resp.setContentType("text/html;charset=UTF-8"); out.write("<html>"); out.write("<head><title>File Index for " + normalized + "</title></head>"); out.write("<body>"); out.write("<table>"); SimpleDateFormat sdf = new SimpleDateFormat(); sdf.applyPattern(dateFormat); for (File child : f.listFiles()) { if (child.isDirectory() && !checkDirectory(child)) { continue; } StringBuffer sb = new StringBuffer(); sb.append("<tr><td>"); sb.append("<a href=\""); if (req.getRequestURL().charAt(req.getRequestURL().length() - 1) != '/') { sb.append(req.getRequestURL().append("/").append(child.getName())); } else { sb.append(req.getRequestURL().append(child.getName())); } sb.append("\">"); sb.append(child.getName()); sb.append("</a>"); sb.append("</td><td>"); sb.append(formatLength(child.length())); sb.append("</td><td>"); sb.append(sdf.format(child.lastModified())); sb.append("</td>"); sb.append("</tr>"); out.write(sb.toString()); } out.write("</table>"); out.write("</body>"); out.write("</html>"); } else { logger.debug("Error state for '{}', returning HTTP 404", f.getAbsolutePath()); resp.sendError(HttpServletResponse.SC_NOT_FOUND); } } protected boolean checkDirectory(File directory) { // If security is off then everyone has access! if (securityService == null) { return true; } boolean allowed = false; File aclFile = null; try { String[] pathBits = directory.getAbsolutePath().split("" + File.separatorChar); aclFile = new File(directory, pathBits[pathBits.length - 1] + ".acl"); allowed = isUserAllowed(aclFile); } catch (IOException e) { logger.debug("Unable to read file " + aclFile.getAbsolutePath() + ", denying access by default"); } catch (SAXException e) { if (aclFile.isFile()) { logger.warn("Invalid XML in file " + aclFile.getAbsolutePath() + ", denying access by default"); } } catch (XPathExpressionException e) { logger.error("Wrong xPath expression: {}", e); } return allowed; } protected boolean isUserAllowed(File aclFile) throws SAXException, IOException, XPathExpressionException { Document aclDoc = builder.parse(aclFile); XPath xPath = XPathFactory.newInstance().newXPath(); NodeList roles = (NodeList) xPath.evaluate("//*[local-name() = 'role']", aclDoc, XPathConstants.NODESET); for (int i = 0; i < roles.getLength(); i++) { Node role = roles.item(i); for (Role userRole : securityService.getUser().getRoles()) { if (userRole.getName().equals(role.getTextContent())) { return true; } } } return false; } protected String formatLength(long length) { // FIXME: Why isn't there a library function for this?! // TODO: Make this better if (length > 1073741824.0) { return length / 1073741824 + " GB"; } else if (length > 1048576.0) { return length / 1048576 + " MB"; } else if (length > 1024.0) { return length / 1024 + " KB"; } else { return length + " B"; } } }