Java tutorial
/* * Copyright 1999,2004 The Apache Software Foundation. * * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0 * * 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.icatproject.idav.methods; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.URLDecoder; import java.text.SimpleDateFormat; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.Locale; import java.util.TimeZone; import javax.servlet.ServletException; 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 org.apache.commons.lang3.StringUtils; import org.icatproject.idav.IMethodExecutor; import org.icatproject.idav.StoredObject; import org.icatproject.idav.WebdavStatus; import org.icatproject.idav.exceptions.LockFailedException; import org.icatproject.idav.fromcatalina.RequestUtil; import org.icatproject.idav.fromcatalina.URLEncoder; import org.icatproject.idav.fromcatalina.XMLWriter; import org.icatproject.idav.locking.IResourceLocks; import org.icatproject.idav.locking.LockedObject; public abstract class AbstractMethod implements IMethodExecutor { private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(AbstractMethod.class); /** * Array containing the safe characters set. */ protected static URLEncoder URL_ENCODER; /** * Default depth is infite. */ protected static final int INFINITY = 3; /** * Simple date format for the creation date ISO 8601 representation * (partial). */ protected static final SimpleDateFormat CREATION_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); /** * Simple date format for the last modified date. (RFC 822 updated by RFC * 1123) */ protected static final SimpleDateFormat LAST_MODIFIED_DATE_FORMAT = new SimpleDateFormat( "EEE, dd MMM yyyy HH:mm:ss z", Locale.US); static { CREATION_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); LAST_MODIFIED_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); /** * GMT timezone - all HTTP dates are on GMT */ URL_ENCODER = new URLEncoder(); URL_ENCODER.addSafeCharacter('-'); URL_ENCODER.addSafeCharacter('_'); URL_ENCODER.addSafeCharacter('.'); URL_ENCODER.addSafeCharacter('*'); URL_ENCODER.addSafeCharacter('/'); } /** * size of the io-buffer */ protected static int BUF_SIZE = 65536; /** * Default lock timeout value. */ protected static final int DEFAULT_TIMEOUT = 3600; /** * Maximum lock timeout. */ protected static final int MAX_TIMEOUT = 604800; /** * Boolean value to temporary lock resources (for method locks) */ protected static final boolean TEMPORARY = true; /** * Timeout for temporary locks */ protected static final int TEMP_TIMEOUT = 10; /** * Return the relative path associated with this servlet. * * @param request * The servlet request we are processing */ protected String getRelativePath(HttpServletRequest request) { // Are we being processed by a RequestDispatcher.include()? if (request.getAttribute("javax.servlet.include.request_uri") != null) { String result = (String) request.getAttribute("javax.servlet.include.path_info"); // if (result == null) // result = (String) request // .getAttribute("javax.servlet.include.servlet_path"); if ((result == null) || (result.equals(""))) result = "/"; return (result); } // KP 17/07/15 - added this section because previously the simple // solution of using request.getPathInfo() was being used but // this starts to fall over when you use special (but legal) // characters in your folder and filenames // we should aim to cope with all of these if at all possible // LOG.trace("getPathInfo: [" + request.getPathInfo() + "]"); // LOG.trace("getPathTranslated: [" + request.getPathTranslated() + "]"); // LOG.trace("getContextPath: [" + request.getContextPath() + "]"); // LOG.trace("getQueryString: [" + request.getQueryString() + "]"); // LOG.trace("getRequestURI: [" + request.getRequestURI() + "]"); // LOG.trace("getRequestURL: [" + request.getRequestURL() + "]"); String requestURI = request.getRequestURI(); String contextPath = request.getContextPath(); if (contextPath != null) { // remove the context path from the request URI requestURI = StringUtils.replaceOnce(requestURI, contextPath, ""); } // I think Windows is not encoding the "+" in folder/file names // so we need to encode this first otherwise it gets changed to a // space by the URLDecoder requestURI = StringUtils.replace(requestURI, "+", "%2B"); try { requestURI = URLDecoder.decode(requestURI, "UTF-8"); } catch (UnsupportedEncodingException e) { // this should never happen - the w3c recommends that UTF-8 encoding is used // see http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars } LOG.trace("Correct requestURI is: [" + requestURI + "]"); String result = requestURI; // end of section added by KP // No, extract the desired path directly from the request // KP 17/07/15 - I commented out the line below but // the 3 lines below that were already commented out //String result = request.getPathInfo(); // if (result == null) { // result = request.getServletPath(); // } if ((result == null) || (result.equals(""))) { result = "/"; } return (result); } /** * Parses and normalizes the destination header. * * @param req * Servlet request * @param resp * Servlet response * @return destinationPath * @throws IOException * if an error occurs while sending response */ protected String parseDestinationHeader(HttpServletRequest req, HttpServletResponse resp) throws IOException { String destinationPath = req.getHeader("Destination"); if (destinationPath == null) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return null; } // Remove url encoding from destination destinationPath = RequestUtil.URLDecode(destinationPath, "UTF8"); int protocolIndex = destinationPath.indexOf("://"); if (protocolIndex >= 0) { // if the Destination URL contains the protocol, we can safely // trim everything upto the first "/" character after "://" int firstSeparator = destinationPath.indexOf("/", protocolIndex + 4); if (firstSeparator < 0) { destinationPath = "/"; } else { destinationPath = destinationPath.substring(firstSeparator); } } else { String hostName = req.getServerName(); if ((hostName != null) && (destinationPath.startsWith(hostName))) { destinationPath = destinationPath.substring(hostName.length()); } int portIndex = destinationPath.indexOf(":"); if (portIndex >= 0) { destinationPath = destinationPath.substring(portIndex); } if (destinationPath.startsWith(":")) { int firstSeparator = destinationPath.indexOf("/"); if (firstSeparator < 0) { destinationPath = "/"; } else { destinationPath = destinationPath.substring(firstSeparator); } } } // Normalize destination path (remove '.' and' ..') destinationPath = normalize(destinationPath); String contextPath = req.getContextPath(); if ((contextPath != null) && (destinationPath.startsWith(contextPath))) { destinationPath = destinationPath.substring(contextPath.length()); } String pathInfo = req.getPathInfo(); if (pathInfo != null) { String servletPath = req.getServletPath(); if ((servletPath != null) && (destinationPath.startsWith(servletPath))) { destinationPath = destinationPath.substring(servletPath.length()); } } return destinationPath; } /** * Return a context-relative path, beginning with a "/", that represents the * canonical version of the specified path after ".." and "." elements are * resolved out. If the specified path attempts to go outside the boundaries * of the current context (i.e. too many ".." path elements are present), * return <code>null</code> instead. * * @param path * Path to be normalized * @return normalized path */ protected String normalize(String path) { if (path == null) return null; // Create a place for the normalized path String normalized = path; if (normalized.equals("/.")) return "/"; // Normalize the slashes and add leading slash if necessary if (normalized.indexOf('\\') >= 0) normalized = normalized.replace('\\', '/'); if (!normalized.startsWith("/")) normalized = "/" + normalized; // Resolve occurrences of "//" in the normalized path while (true) { int index = normalized.indexOf("//"); if (index < 0) break; normalized = normalized.substring(0, index) + normalized.substring(index + 1); } // Resolve occurrences of "/./" in the normalized path while (true) { int index = normalized.indexOf("/./"); if (index < 0) break; normalized = normalized.substring(0, index) + normalized.substring(index + 2); } // Resolve occurrences of "/../" in the normalized path while (true) { int index = normalized.indexOf("/../"); if (index < 0) break; if (index == 0) return (null); // Trying to go outside our context int index2 = normalized.lastIndexOf('/', index - 1); normalized = normalized.substring(0, index2) + normalized.substring(index + 3); } // Return the normalized path that we have completed return (normalized); } protected void logRequestAttributes(HttpServletRequest request) { Enumeration<String> attrNames = request.getAttributeNames(); LOG.trace("--- HttpServletRequest attribute values ---"); while (attrNames.hasMoreElements()) { String attrName = attrNames.nextElement(); String attrValue = request.getAttribute(attrName).toString(); LOG.trace(attrName + " : " + attrValue); } LOG.trace("-------------------------------------------"); } /** * creates the parent path from the given path by removing the last '/' and * everything after that * * @param path * the path * @return parent path */ protected String getParentPath(String path) { int slash = path.lastIndexOf('/'); if (slash != -1) { return path.substring(0, slash); } return null; } /** * removes a / at the end of the path string, if present * * @param path * the path * @return the path without trailing / */ protected String getCleanPath(String path) { if (path.endsWith("/") && path.length() > 1) path = path.substring(0, path.length() - 1); return path; } /** * Return JAXP document builder instance. */ protected DocumentBuilder getDocumentBuilder() throws ServletException { DocumentBuilder documentBuilder = null; DocumentBuilderFactory documentBuilderFactory = null; try { documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); // disable XML External Entities to prevent XML XXE attacks as described at: // https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); documentBuilderFactory.setXIncludeAware(false); documentBuilderFactory.setExpandEntityReferences(false); // documentBuilder = documentBuilderFactory.newDocumentBuilder(); } catch (ParserConfigurationException e) { String msg = "Failed to create an XML DocumentBuilder which satisfies the configuration requested: " + e.getMessage(); LOG.error(msg); throw new ServletException(msg); } return documentBuilder; } /** * reads the depth header from the request and returns it as a int * * @param req * @return the depth from the depth header */ protected int getDepth(HttpServletRequest req) { int depth = INFINITY; String depthStr = req.getHeader("Depth"); if (depthStr != null) { if (depthStr.equals("0")) { depth = 0; } else if (depthStr.equals("1")) { depth = 1; } } return depth; } /** * URL rewriter. * * @param path * Path which has to be rewiten * @return the rewritten path */ protected String rewriteUrl(String path) { return URL_ENCODER.encode(path); } /** * Get the ETag associated with a file. * * @param so * StoredObject to get resourceLength, lastModified and a hashCode of * StoredObject * @return the ETag */ protected String getETag(StoredObject so) { String resourceLength = ""; String lastModified = ""; if (so != null && so.isResource()) { resourceLength = new Long(so.getResourceLength()).toString(); lastModified = new Long(so.getLastModified().getTime()).toString(); } return "W/\"" + resourceLength + "-" + lastModified + "\""; } protected String[] getLockIdFromIfHeader(HttpServletRequest req) { String[] ids = new String[2]; String id = req.getHeader("If"); if (id != null && !id.equals("")) { if (id.indexOf(">)") == id.lastIndexOf(">)")) { id = id.substring(id.indexOf("(<"), id.indexOf(">)")); if (id.indexOf("locktoken:") != -1) { id = id.substring(id.indexOf(':') + 1); } ids[0] = id; } else { String firstId = id.substring(id.indexOf("(<"), id.indexOf(">)")); if (firstId.indexOf("locktoken:") != -1) { firstId = firstId.substring(firstId.indexOf(':') + 1); } ids[0] = firstId; String secondId = id.substring(id.lastIndexOf("(<"), id.lastIndexOf(">)")); if (secondId.indexOf("locktoken:") != -1) { secondId = secondId.substring(secondId.indexOf(':') + 1); } ids[1] = secondId; } } else { ids = null; } return ids; } protected String getLockIdFromLockTokenHeader(HttpServletRequest req) { String id = req.getHeader("Lock-Token"); if (id != null) { id = id.substring(id.indexOf(":") + 1, id.indexOf(">")); } return id; } /** * Checks if locks on resources at the given path exists and if so checks * the If-Header to make sure the If-Header corresponds to the locked * resource. Returning true if no lock exists or the If-Header is * corresponding to the locked resource * * @param req * Servlet request * @param resp * Servlet response * @param resourceLocks * @param path * path to the resource * @return true if no lock on a resource with the given path exists or if * the If-Header corresponds to the locked resource * @throws IOException * @throws LockFailedException */ protected boolean checkLocks(String authString, HttpServletRequest req, HttpServletResponse resp, IResourceLocks resourceLocks, String path) throws IOException, LockFailedException { LockedObject loByPath = resourceLocks.getLockedObjectByPath(authString, path); if (loByPath != null) { if (loByPath.isShared()) return true; // the resource is locked String[] lockTokens = getLockIdFromIfHeader(req); String lockToken = null; if (lockTokens != null) lockToken = lockTokens[0]; else { return false; } if (lockToken != null) { LockedObject loByIf = resourceLocks.getLockedObjectByID(authString, lockToken); if (loByIf == null) { // no locked resource to the given lockToken return false; } if (!loByIf.equals(loByPath)) { loByIf = null; return false; } loByIf = null; } } loByPath = null; return true; } /** * Send a multistatus element containing a complete error report to the * client. * * @param req * Servlet request * @param resp * Servlet response * @param errorList * List of error to be displayed */ protected void sendReport(HttpServletRequest req, HttpServletResponse resp, Hashtable<String, Integer> errorList) throws IOException { resp.setStatus(WebdavStatus.SC_MULTI_STATUS); // String absoluteUri = req.getRequestURI(); // String relativePath = getRelativePath(req); HashMap<String, String> namespaces = new HashMap<String, String>(); namespaces.put("DAV:", "D"); XMLWriter generatedXML = new XMLWriter(namespaces); generatedXML.writeXMLHeader(); generatedXML.writeElement("DAV::multistatus", XMLWriter.OPENING); Enumeration<String> pathList = errorList.keys(); while (pathList.hasMoreElements()) { String errorPath = (String) pathList.nextElement(); int errorCode = ((Integer) errorList.get(errorPath)).intValue(); generatedXML.writeElement("DAV::response", XMLWriter.OPENING); generatedXML.writeElement("DAV::href", XMLWriter.OPENING); // String toAppend = null; // if (absoluteUri.endsWith(errorPath)) { // toAppend = absoluteUri; // // } else if (absoluteUri.contains(errorPath)) { // // int endIndex = absoluteUri.indexOf(errorPath) // + errorPath.length(); // toAppend = absoluteUri.substring(0, endIndex); // } // // KP 18/03/15 - when I edit a file and resave it I get a NullPointerException // // in the log from the line below. I can't see how "toAppend" is being // // used - it appears to be set but then not used - so I am commenting out // // this whole section to see what happens // if (!toAppend.startsWith("/") && !toAppend.startsWith("http:")) // toAppend = "/" + toAppend; generatedXML.writeText(errorPath); generatedXML.writeElement("DAV::href", XMLWriter.CLOSING); generatedXML.writeElement("DAV::status", XMLWriter.OPENING); generatedXML.writeText("HTTP/1.1 " + errorCode + " " + WebdavStatus.getStatusText(errorCode)); generatedXML.writeElement("DAV::status", XMLWriter.CLOSING); generatedXML.writeElement("DAV::response", XMLWriter.CLOSING); } generatedXML.writeElement("DAV::multistatus", XMLWriter.CLOSING); Writer writer = resp.getWriter(); writer.write(generatedXML.toString()); writer.close(); } }