/** * Copyright (c) 2008, Aberystwyth University * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * - Neither the name of the Centre for Advanced Software and * Intelligent Systems (CASIS) nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ package org.purl.sword.server; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicInteger; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import org.purl.sword.atom.Summary; import org.purl.sword.atom.Title; import org.purl.sword.base.ChecksumUtils; import org.purl.sword.base.Deposit; import org.purl.sword.base.DepositResponse; import org.purl.sword.base.ErrorCodes; import org.purl.sword.base.HttpHeaders; import org.purl.sword.base.SWORDAuthenticationException; import org.purl.sword.base.SWORDErrorDocument; import org.purl.sword.base.SWORDException; import org.purl.sword.base.SWORDErrorException; /** * DepositServlet * * @author Stuart Lewis */ public class DepositServlet extends HttpServlet { /** Sword repository */ protected SWORDServer myRepository; /** Authentication type */ private String authN; /** Maximum file upload size in kB **/ private int maxUploadSize; /** Temp directory */ private String tempDirectory; /** Counter */ private static AtomicInteger counter = new AtomicInteger(0); /** Logger */ private static Logger log = Logger.getLogger(DepositServlet.class); /** * Initialise the servlet * * @throws ServletException */ public void init() throws ServletException { // Instantiate the correct SWORD Server class String className = getServletContext().getInitParameter("sword-server-class"); if (className == null) { log.fatal("Unable to read value of 'sword-server-class' from Servlet context"); } else { try { myRepository = (SWORDServer) Class.forName(className) .newInstance(); log.info("Using " + className + " as the SWORDServer"); } catch (Exception e) { log .fatal("Unable to instantiate class from 'sword-server-class': " + className); throw new ServletException( "Unable to instantiate class from 'sword-server-class': " + className); } } authN = getServletContext().getInitParameter("authentication-method"); if ((authN == null) || (authN.equals(""))) { authN = "None"; } log.info("Authentication type set to: " + authN); String maxUploadSizeStr = getServletContext().getInitParameter("maxUploadSize"); if ((maxUploadSizeStr == null) || (maxUploadSizeStr.equals("")) || (maxUploadSizeStr.equals("-1"))) { maxUploadSize = -1; log.warn("No maxUploadSize set, so setting max file upload size to unlimited."); } else { try { maxUploadSize = Integer.parseInt(maxUploadSizeStr); log.info("Setting max file upload size to " + maxUploadSize); } catch (NumberFormatException nfe) { maxUploadSize = -1; log.warn("maxUploadSize not a number, so setting max file upload size to unlimited."); } } tempDirectory = getServletContext().getInitParameter( "upload-temp-directory"); if ((tempDirectory == null) || (tempDirectory.equals(""))) { tempDirectory = System.getProperty("java.io.tmpdir"); } if (!tempDirectory.endsWith(System.getProperty("file.separator"))) { tempDirectory += System.getProperty("file.separator"); } File tempDir = new File(tempDirectory); log.info("Upload temporary directory set to: " + tempDir); if (!tempDir.exists()) { if (!tempDir.mkdirs()) { throw new ServletException( "Upload directory did not exist and I can't create it. " + tempDir); } } if (!tempDir.isDirectory()) { log.fatal("Upload temporary directory is not a directory: " + tempDir); throw new ServletException( "Upload temporary directory is not a directory: " + tempDir); } if (!tempDir.canWrite()) { log.fatal("Upload temporary directory cannot be written to: " + tempDir); throw new ServletException( "Upload temporary directory cannot be written to: " + tempDir); } } /** * Process the Get request. This will return an unimplemented response. */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Send a '501 Not Implemented' response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); } /** * Process a post request. */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Create the Deposit request Deposit d = new Deposit(); Date date = new Date(); log.debug("Starting deposit processing at " + date.toString() + " by " + request.getRemoteAddr()); // Are there any authentication details? String usernamePassword = getUsernamePassword(request); if ((usernamePassword != null) && (!usernamePassword.equals(""))) { int p = usernamePassword.indexOf(":"); if (p != -1) { d.setUsername(usernamePassword.substring(0, p)); d.setPassword(usernamePassword.substring(p + 1)); } } else if (authenticateWithBasic()) { String s = "Basic realm=\"SWORD\""; response.setHeader("WWW-Authenticate", s); response.setStatus(401); return; } // Set up some variables String filename = null; File f = null; FileInputStream fis = null; // Do the processing try { // Write the file to the temp directory filename = tempDirectory + "SWORD-" + request.getRemoteAddr() + "-" + counter.addAndGet(1); log.debug("Package temporarily stored as: " + filename); InputStream inputstream = request.getInputStream(); OutputStream outputstream = new FileOutputStream(new File(filename)); try { byte[] buf = new byte[1024]; int len; while ((len = inputstream.read(buf)) > 0) { outputstream.write(buf, 0, len); } } finally { inputstream.close(); outputstream.close(); } // Check the size is OK File file = new File(filename); long fLength = file.length() / 1024; if ((maxUploadSize != -1) && (fLength > maxUploadSize)) { this.makeErrorDocument(ErrorCodes.MAX_UPLOAD_SIZE_EXCEEDED, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, "The uploaded file exceeded the maximum file size this server will accept (the file is " + fLength + "kB but the server will only accept files as large as " + maxUploadSize + "kB)", request, response); return; } // Check the MD5 hash String receivedMD5 = ChecksumUtils.generateMD5(filename); log.debug("Received filechecksum: " + receivedMD5); d.setMd5(receivedMD5); String md5 = request.getHeader("Content-MD5"); log.debug("Received file checksum header: " + md5); if ((md5 != null) && (!md5.equals(receivedMD5))) { // Return an error document this.makeErrorDocument(ErrorCodes.ERROR_CHECKSUM_MISMATCH, HttpServletResponse.SC_PRECONDITION_FAILED, "The received MD5 checksum for the deposited file did not match the checksum sent by the deposit client", request, response); log.debug("Bad MD5 for file. Aborting with appropriate error message"); return; } else { // Set the file f = new File(filename); fis = new FileInputStream(f); d.setFile(fis); // Set the X-On-Behalf-Of header String onBehalfOf = request.getHeader(HttpHeaders.X_ON_BEHALF_OF.toString()); if ((onBehalfOf != null) && (onBehalfOf.equals("reject"))) { // user name is "reject", so throw a not know error to allow the client to be tested throw new SWORDErrorException(ErrorCodes.TARGET_OWNER_UKNOWN,"unknown user \"reject\""); } else { d.setOnBehalfOf(onBehalfOf); } // Set the X-Packaging header d.setPackaging(request.getHeader(HttpHeaders.X_PACKAGING)); // Set the X-No-Op header String noop = request.getHeader(HttpHeaders.X_NO_OP); log.error("X_NO_OP value is " + noop); if ((noop != null) && (noop.equals("true"))) { d.setNoOp(true); } else if ((noop != null) && (noop.equals("false"))) { d.setNoOp(false); }else if (noop == null) { d.setNoOp(false); } else { throw new SWORDErrorException(ErrorCodes.ERROR_BAD_REQUEST,"Bad no-op"); } // Set the X-Verbose header String verbose = request.getHeader(HttpHeaders.X_VERBOSE); if ((verbose != null) && (verbose.equals("true"))) { d.setVerbose(true); } else if ((verbose != null) && (verbose.equals("false"))) { d.setVerbose(false); }else if (verbose == null) { d.setVerbose(false); } else { throw new SWORDErrorException(ErrorCodes.ERROR_BAD_REQUEST,"Bad verbose"); } // Set the slug String slug = request.getHeader(HttpHeaders.SLUG); if (slug != null) { d.setSlug(slug); } // Set the content disposition d.setContentDisposition(request.getHeader(HttpHeaders.CONTENT_DISPOSITION)); // Set the IP address d.setIPAddress(request.getRemoteAddr()); // Set the deposit location d.setLocation(getUrl(request)); // Set the content type d.setContentType(request.getContentType()); // Set the content length String cl = request.getHeader(HttpHeaders.CONTENT_LENGTH); if ((cl != null) && (!cl.equals(""))) { d.setContentLength(Integer.parseInt(cl)); } // Get the DepositResponse DepositResponse dr = myRepository.doDeposit(d); // Echo back the user agent if (request.getHeader(HttpHeaders.USER_AGENT.toString()) != null) { dr.getEntry().setUserAgent(request.getHeader(HttpHeaders.USER_AGENT.toString())); } // Echo back the packaging format if (request.getHeader(HttpHeaders.X_PACKAGING.toString()) != null) { dr.getEntry().setPackaging(request.getHeader(HttpHeaders.X_PACKAGING.toString())); } // Print out the Deposit Response response.setStatus(dr.getHttpResponse()); if ((dr.getLocation() != null) && (!dr.getLocation().equals(""))) { response.setHeader("Location", dr.getLocation()); } response.setContentType("application/atom+xml; charset=UTF-8"); PrintWriter out = response.getWriter(); out.write(dr.marshall()); out.flush(); } } catch (SWORDAuthenticationException sae) { // Ask for credentials again if (authN.equals("Basic")) { String s = "Basic realm=\"SWORD\""; response.setHeader("WWW-Authenticate", s); response.setStatus(401); } } catch (SWORDErrorException see) { // Get the details and send the right SWORD error document log.error(see.toString()); this.makeErrorDocument(see.getErrorURI(), see.getStatus(), see.getDescription(), request, response); return; } catch (SWORDException se) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); log.error(se.toString()); } catch (NoSuchAlgorithmException nsae) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); log.error(nsae.toString()); } finally { // Close the input stream if it still open if (fis != null) { fis.close(); } // Try deleting the temp file if (filename != null) { f = new File(filename); f.delete(); } } } /** * Utility method to construct a SWORDErrorDocumentTest * * @param errorURI The error URI to pass * @param status The HTTP status to return * @param summary The textual description to give the user * @param request The HttpServletRequest object * @param response The HttpServletResponse to send the error document to * @throws IOException */ protected void makeErrorDocument(String errorURI, int status, String summary, HttpServletRequest request, HttpServletResponse response) throws IOException { SWORDErrorDocument sed = new SWORDErrorDocument(errorURI); Title title = new Title(); title.setContent("ERROR"); sed.setTitle(title); Calendar calendar = Calendar.getInstance(); String utcformat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; SimpleDateFormat zulu = new SimpleDateFormat(utcformat); String serializeddate = zulu.format(calendar.getTime()); sed.setUpdated(serializeddate); Summary sum = new Summary(); sum.setContent(summary); sed.setSummary(sum); if (request.getHeader(HttpHeaders.USER_AGENT.toString()) != null) { sed.setUserAgent(request.getHeader(HttpHeaders.USER_AGENT.toString())); } response.setStatus(status); response.setContentType("application/atom+xml; charset=UTF-8"); PrintWriter out = response.getWriter(); out.write(sed.marshall().toXML()); out.flush(); } /** * Utility method to return the username and password (separated by a colon * ':') * * @param request * @return The username and password combination */ protected String getUsernamePassword(HttpServletRequest request) { try { String authHeader = request.getHeader("Authorization"); if (authHeader != null) { StringTokenizer st = new StringTokenizer(authHeader); if (st.hasMoreTokens()) { String basic = st.nextToken(); if (basic.equalsIgnoreCase("Basic")) { String credentials = st.nextToken(); String userPass = new String(Base64 .decodeBase64(credentials.getBytes())); return userPass; } } } } catch (Exception e) { log.debug(e.toString()); } return null; } /** * Utility method to decide if we are using HTTP Basic authentication * * @return if HTTP Basic authentication is in use or not */ protected boolean authenticateWithBasic() { if (authN.equalsIgnoreCase("Basic")) { return true; } else { return false; } } /** * Utility method to construct the URL called for this Servlet * * @param req The request object * @return The URL */ protected static String getUrl(HttpServletRequest req) { String reqUrl = req.getRequestURL().toString(); String queryString = req.getQueryString(); if (queryString != null) { reqUrl += "?" + queryString; } return reqUrl; } }