Java tutorial
/* * Copyright 2007 Jesse Peterson * * 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 com.jpeterson.littles3; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Writer; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.AccessControlException; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.TimeZone; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Hex; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.web.servlet.FrameworkServlet; import com.jpeterson.littles3.bo.Acp; import com.jpeterson.littles3.bo.AllUsersGroup; import com.jpeterson.littles3.bo.AuthenticatedUsersGroup; import com.jpeterson.littles3.bo.Authenticator; import com.jpeterson.littles3.bo.AuthenticatorException; import com.jpeterson.littles3.bo.Bucket; import com.jpeterson.littles3.bo.CanonicalUser; import com.jpeterson.littles3.bo.InvalidAccessKeyIdException; import com.jpeterson.littles3.bo.InvalidSecurityException; import com.jpeterson.littles3.bo.RequestTimeTooSkewedException; import com.jpeterson.littles3.bo.ResourcePermission; import com.jpeterson.littles3.bo.S3Object; import com.jpeterson.littles3.bo.SignatureDoesNotMatchException; import com.jpeterson.littles3.service.BucketAlreadyExistsException; import com.jpeterson.littles3.service.BucketNotEmptyException; import com.jpeterson.littles3.service.StorageService; import com.jpeterson.util.etag.ETag; import com.jpeterson.util.etag.FileETag; import com.jpeterson.util.http.Range; import com.jpeterson.util.http.RangeFactory; import com.jpeterson.util.http.RangeInputStream; import com.jpeterson.util.http.RangeSet; public class StorageEngine extends FrameworkServlet { /** * */ private static final long serialVersionUID = 1L; /** * HTTP Header that can be used to override the actual method. Useful in * situations, for instance, where a firewall only allows "GET" AND "POST" * methods, but you need to use "PUT" and "DELETE" methods. You can specify * this HTTP header and the appropriate value. */ public static final String HEADER_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override"; public static final String HEADER_PREFIX_USER_META = "x-amz-meta-"; private Log logger; /** * Default configuration file name. */ public static final String DEFAULT_CONFIGURATION = "StorageEngine.properties"; /** * Configuration property defining the HTTP Host that this engine is * serving. */ public static final String CONFIG_HOST = "host"; /** * This token can be used in a <code>CONFIG_HOST</code> for the local host. * It is resolved via * <code>InetAddress.getLocalHost().getCanonicalHostName()</code>. */ public static final String CONFIG_HOST_TOKEN_RESOLVED_LOCAL_HOST = "$resolvedLocalHost$"; public static final String BEAN_AUTHENTICATOR = "authenticator"; public static final String BEAN_STORAGE_SERVICE = "storageService"; private ETag eTag; private Configuration configuration; private static SimpleDateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); private static TimeZone utc = TimeZone.getTimeZone("UTC"); static { iso8601.setTimeZone(utc); } private static final String HEADER_X_AMZ_ACL = "x-amz-acl"; private static final String ACL_PRIVATE = "private"; private static final String ACL_PUBLIC_READ = "public-read"; private static final String ACL_PUBLIC_READ_WRITE = "public-read-write"; private static final String ACL_AUTHENTICATED_READ = "authenticated-read"; private static final String PARAMETER_ACL = "acl"; /** * Basic constructor. Initializes the logger. */ public StorageEngine() { super(); logger = LogFactory.getLog(this.getClass()); } /** * Initialize the servlet. * * @throws ServletException * if an exception occurs that interrupts the servlet's normal * operation */ public void initFrameworkServlet() throws ServletException { FileETag eTag = new FileETag(); eTag.setFlags(FileETag.FLAG_CONTENT); setETag(eTag); try { configuration = new PropertiesConfiguration(DEFAULT_CONFIGURATION); } catch (ConfigurationException e) { logger.warn("Unable to load default properties-based configuration: " + DEFAULT_CONFIGURATION); configuration = new PropertiesConfiguration(); } } public void destroy() { super.destroy(); } /** * Get the ETag calculator. * * @return The ETag calculator. */ public ETag getETag() { return eTag; } /** * Set the ETag calculator. * * @param eTag * The ETag calculator. */ public void setETag(ETag eTag) { this.eTag = eTag; } /** * Subclasses must implement this method to do the work of request handling, * receiving a centralized callback for GET, POST, PUT and DELETE. * * @param request * current HTTP request * @param response * current HTTP response * @throws Exception * in case of any kind of processing failure */ protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { String method; method = getMethod(request); logger.debug("Method: " + method); if (method.equalsIgnoreCase("GET")) { // read methodGet(request, response); } else if (method.equalsIgnoreCase("HEAD")) { // headers methodHead(request, response); } else if (method.equalsIgnoreCase("PUT")) { // create methodPut(request, response); } else if (method.equalsIgnoreCase("DELETE")) { // remove methodDelete(request, response); } } /** * Returns the HTTP method of the request. Implements logic to allow an * "override" method, specified by the header * <code>HEADER_HTTP_METHOD_OVERRIDE</code>. If the override method is * provided, it takes precedence over the actual method derived from * <code>request.getMethod()</code>. * * @param request * The request being processed. * @return The method of the request. * @see #HEADER_HTTP_METHOD_OVERRIDE */ public static String getMethod(HttpServletRequest request) { String method; method = request.getHeader(HEADER_HTTP_METHOD_OVERRIDE); if (method == null) { method = request.getMethod(); } return method; } /** * Metadata * * @param req * the request object that is passed to the servlet * @param resp * the response object that the servlet uses to return the * headers to the client * @throws IOException * if an input or output error occurs * @throws ServletException * if the request for the HEAD could not be handled */ public void methodHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // write the body. the servlet container makes sure to not send the body // for the HEAD processHeadGet(req, resp); } /** * Read * * @param req * an HttpServletRequest object that contains the request the * client has made of the servlet * @param resp * an HttpServletResponse object that contains the response the * servlet sends to the client * @throws IOException * if an input or output error is detected when the servlet * handles the GET request * @throws ServletException * if the request for the GET could not be handled */ public void methodGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { processHeadGet(req, resp); } /** * Process HTTP HEAD and GET * * @param req * an HttpServletRequest object that contains the request the * client has made of the servlet * @param resp * an HttpServletResponse object that contains the response the * servlet sends to the client * @throws IOException * if an input or output error is detected when the servlet * handles the GET request * @throws ServletException * if the request for the GET could not be handled */ @SuppressWarnings("unchecked") public void processHeadGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (logger.isDebugEnabled()) { logger.debug("Context path: " + req.getContextPath()); logger.debug("Path info: " + req.getPathInfo()); logger.debug("Path translated: " + req.getPathTranslated()); logger.debug("Query string: " + req.getQueryString()); logger.debug("Request URI: " + req.getRequestURI()); logger.debug("Request URL: " + req.getRequestURL()); logger.debug("Servlet path: " + req.getServletPath()); logger.debug("Servlet name: " + this.getServletName()); for (Enumeration headerNames = req.getHeaderNames(); headerNames.hasMoreElements();) { String headerName = (String) headerNames.nextElement(); String headerValue = req.getHeader(headerName); logger.debug("Header- " + headerName + ": " + headerValue); } } try { S3ObjectRequest or; try { or = S3ObjectRequest.create(req, resolvedHost(), (Authenticator) getWebApplicationContext().getBean(BEAN_AUTHENTICATOR)); } catch (InvalidAccessKeyIdException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidAccessKeyId"); return; } catch (InvalidSecurityException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidSecurity"); return; } catch (RequestTimeTooSkewedException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "RequestTimeTooSkewed"); return; } catch (SignatureDoesNotMatchException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "SignatureDoesNotMatch"); return; } catch (AuthenticatorException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidSecurity"); return; } if (or.getKey() != null) { S3Object s3Object; StorageService storageService; try { storageService = (StorageService) getWebApplicationContext().getBean(BEAN_STORAGE_SERVICE); s3Object = storageService.load(or.getBucket(), or.getKey()); if (s3Object == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchKey"); return; } } catch (DataAccessException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchKey"); return; } if (req.getParameter(PARAMETER_ACL) != null) { // retrieve access control policy String response; Acp acp = s3Object.getAcp(); try { acp.canRead(or.getRequestor()); } catch (AccessControlException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } response = Acp.encode(acp); resp.setContentLength(response.length()); resp.setContentType("application/xml"); resp.setStatus(HttpServletResponse.SC_OK); Writer out = resp.getWriter(); out.write(response); out.flush(); // commit response out.close(); out = null; } else { // retrieve object InputStream in = null; OutputStream out = null; byte[] buffer = new byte[4096]; int count; String value; try { s3Object.canRead(or.getRequestor()); } catch (AccessControlException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } // headers resp.setContentType(s3Object.getContentType()); if ((value = s3Object.getContentDisposition()) != null) { resp.setHeader("Content-Disposition", value); } // TODO: set the Content-Range, if request includes Range // TODO: add "x-amz-missing-meta", if any // add the "x-amz-meta-" headers for (Iterator<String> names = s3Object.getMetadataNames(); names.hasNext();) { String name = names.next(); String headerName = HEADER_PREFIX_USER_META + name; String prefix = ""; StringBuffer buf = new StringBuffer(); for (Iterator<String> values = s3Object.getMetadataValues(name); values.hasNext();) { buf.append(values.next()).append(prefix); prefix = ","; } resp.setHeader(headerName, buf.toString()); } resp.setDateHeader("Last-Modified", s3Object.getLastModified()); if ((value = s3Object.getETag()) != null) { resp.setHeader("ETag", value); } if ((value = s3Object.getContentMD5()) != null) { resp.setHeader("Content-MD5", value); } if ((value = s3Object.getContentDisposition()) != null) { resp.setHeader("Content-Disposition", value); } resp.setHeader("Accept-Ranges", "bytes"); String rangeRequest = req.getHeader("Range"); if (rangeRequest != null) { // request for a range RangeSet rangeSet = RangeFactory.processRangeHeader(rangeRequest); // set content length rangeSet.resolve(s3Object.getContentLength()); if (rangeSet.size() > 1) { // requires multi-part response // TODO: implement resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); } Range[] ranges = (Range[]) rangeSet.toArray(new Range[0]); resp.setHeader("Content-Range", formatRangeHeaderValue(ranges[0], s3Object.getContentLength())); resp.setHeader("Content-Length", Long.toString(rangeSet.getLength())); in = new RangeInputStream(s3Object.getInputStream(), ranges[0]); resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); } else { // request for entire content // Used instead of resp.setContentLength((int)); because // Amazon // limit is 5 gig, which is bigger than an int resp.setHeader("Content-Length", Long.toString(s3Object.getContentLength())); in = s3Object.getInputStream(); resp.setStatus(HttpServletResponse.SC_OK); } // body out = resp.getOutputStream(); while ((count = in.read(buffer, 0, buffer.length)) > 0) { out.write(buffer, 0, count); } out.flush(); // commit response out.close(); out = null; } return; } else if (or.getBucket() != null) { // operation on a bucket StorageService storageService; String prefix; String marker; int maxKeys = Integer.MAX_VALUE; String delimiter; String response; String value; storageService = (StorageService) getWebApplicationContext().getBean(BEAN_STORAGE_SERVICE); if (req.getParameter(PARAMETER_ACL) != null) { // retrieve access control policy Acp acp; try { acp = storageService.loadBucket(or.getBucket()).getAcp(); } catch (DataAccessException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchBucket"); return; } try { acp.canRead(or.getRequestor()); } catch (AccessControlException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } response = Acp.encode(acp); resp.setContentLength(response.length()); resp.setContentType("application/xml"); resp.setStatus(HttpServletResponse.SC_OK); Writer out = resp.getWriter(); out.write(response); out.flush(); // commit response out.close(); out = null; } else { Bucket bucket; prefix = req.getParameter("prefix"); if (prefix == null) { prefix = ""; } marker = req.getParameter("marker"); value = req.getParameter("max-keys"); if (value != null) { try { maxKeys = Integer.parseInt(value); } catch (NumberFormatException e) { logger.info("max-keys must be numeric: " + value); } } delimiter = req.getParameter("delimiter"); try { bucket = storageService.loadBucket(or.getBucket()); } catch (DataAccessException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchBucket"); return; } try { bucket.canRead(or.getRequestor()); } catch (AccessControlException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } response = storageService.listKeys(bucket, prefix, marker, delimiter, maxKeys); resp.setContentLength(response.length()); resp.setContentType("application/xml"); resp.setStatus(HttpServletResponse.SC_OK); Writer out = resp.getWriter(); out.write(response); if (logger.isTraceEnabled()) { logger.trace("Response: " + response); } } return; } else { // operation on the service StorageService storageService; List buckets; storageService = (StorageService) getWebApplicationContext().getBean(BEAN_STORAGE_SERVICE); buckets = storageService.findBuckets(""); StringBuffer buffer = new StringBuffer(); buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); buffer.append("<ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">"); buffer.append("<Owner>"); buffer.append("<ID/>"); // TODO: implement buffer.append("<DisplayName/>"); // TODO: implementF buffer.append("</Owner>"); buffer.append("<Buckets>"); for (Iterator iter = buckets.iterator(); iter.hasNext();) { Bucket bucket = (Bucket) iter.next(); buffer.append("<Bucket>"); buffer.append("<Name>").append(bucket.getName()).append("</Name>"); buffer.append("<CreationDate>").append(iso8601.format(bucket.getCreated())) .append("</CreationDate>"); buffer.append("</Bucket>"); } buffer.append("</Buckets>"); buffer.append("</ListAllMyBucketsResult>"); resp.setContentLength(buffer.length()); resp.setContentType("application/xml"); resp.setStatus(HttpServletResponse.SC_OK); Writer out = resp.getWriter(); out.write(buffer.toString()); return; } } catch (IllegalArgumentException e) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidURI"); return; } } /** * Write * * @param req * the HttpServletRequest object that contains the request the * client made of the servlet * @param resp * the HttpServletResponse object that contains the response the * servlet returns to the client * @throws IOException * if an input or output error occurs while the servlet is * handling the PUT request * @throws ServletException * if the request for the PUT cannot be handled */ @SuppressWarnings("unchecked") public void methodPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { OutputStream out = null; try { S3ObjectRequest or; try { or = S3ObjectRequest.create(req, resolvedHost(), (Authenticator) getWebApplicationContext().getBean(BEAN_AUTHENTICATOR)); } catch (InvalidAccessKeyIdException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidAccessKeyId"); return; } catch (InvalidSecurityException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidSecurity"); return; } catch (RequestTimeTooSkewedException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "RequestTimeTooSkewed"); return; } catch (SignatureDoesNotMatchException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "SignatureDoesNotMatch"); return; } catch (AuthenticatorException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidSecurity"); return; } logger.debug("S3ObjectRequest: " + or); CanonicalUser requestor = or.getRequestor(); if (or.getKey() != null) { String value; long contentLength; MessageDigest messageDigest = MessageDigest.getInstance("MD5"); DigestOutputStream digestOutputStream = null; S3Object oldS3Object = null; S3Object s3Object; StorageService storageService; Bucket bucket; String bucketName = or.getBucket(); String key = or.getKey(); if (!isValidKey(key)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "KeyTooLong"); return; } storageService = (StorageService) getWebApplicationContext().getBean(BEAN_STORAGE_SERVICE); if (req.getParameter(PARAMETER_ACL) != null) { // write access control policy Acp acp; CanonicalUser owner; s3Object = storageService.load(bucketName, key); if (s3Object == null) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchKey"); return; } acp = s3Object.getAcp(); try { acp.canWrite(requestor); } catch (AccessControlException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } // save owner owner = acp.getOwner(); try { acp = Acp.decode(req.getInputStream()); } catch (IOException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "MalformedACLError"); return; } // maintain owner acp.setOwner(owner); s3Object.setAcp(acp); storageService.store(s3Object); } else { // make sure requestor can "WRITE" to the bucket try { bucket = storageService.loadBucket(bucketName); bucket.canWrite(requestor); } catch (AccessControlException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } catch (DataAccessException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchBucket"); return; } try { oldS3Object = storageService.load(bucket.getName(), key); } catch (DataRetrievalFailureException e) { // ignore } // create a new S3Object for this request to store an object try { s3Object = storageService.createS3Object(bucket, key, requestor); } catch (DataAccessException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchBucket"); return; } out = s3Object.getOutputStream(); digestOutputStream = new DigestOutputStream(out, messageDigest); // Used instead of req.getContentLength(); because Amazon // limit is 5 gig, which is bigger than an int value = req.getHeader("Content-Length"); if (value == null) { resp.sendError(HttpServletResponse.SC_LENGTH_REQUIRED, "MissingContentLength"); return; } contentLength = Long.valueOf(value).longValue(); if (contentLength > 5368709120L) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "EntityTooLarge"); return; } long written = 0; int count; byte[] b = new byte[4096]; ServletInputStream in = req.getInputStream(); while (((count = in.read(b, 0, b.length)) > 0) && (written < contentLength)) { digestOutputStream.write(b, 0, count); written += count; } digestOutputStream.flush(); if (written != contentLength) { // transmission truncated if (out != null) { out.close(); out = null; } if (digestOutputStream != null) { digestOutputStream.close(); digestOutputStream = null; } // clean up storageService.remove(s3Object); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "IncompleteBody"); return; } s3Object.setContentDisposition(req.getHeader("Content-Disposition")); s3Object.setContentLength(contentLength); s3Object.setContentMD5(req.getHeader("Content-MD5")); value = req.getContentType(); logger.debug("Put - Content-Type: " + value); if (value == null) { value = S3Object.DEFAULT_CONTENT_TYPE; } s3Object.setContentType(value); logger.debug("Put - get content-type: " + s3Object.getContentType()); s3Object.setLastModified(System.currentTimeMillis()); // metadata int prefixLength = HEADER_PREFIX_USER_META.length(); String name; for (Enumeration headerNames = req.getHeaderNames(); headerNames.hasMoreElements();) { String headerName = (String) headerNames.nextElement(); if (headerName.startsWith(HEADER_PREFIX_USER_META)) { name = headerName.substring(prefixLength).toLowerCase(); for (Enumeration headers = req.getHeaders(headerName); headers.hasMoreElements();) { value = (String) headers.nextElement(); s3Object.addMetadata(name, value); } } } // calculate ETag, hex encoding of MD5 value = new String(Hex.encodeHex(digestOutputStream.getMessageDigest().digest())); resp.setHeader("ETag", value); s3Object.setETag(value); grantCannedAccessPolicies(req, s3Object.getAcp(), requestor); // NOTE: This could be reengineered to have a two-phase // commit. if (oldS3Object != null) { storageService.remove(oldS3Object); } storageService.store(s3Object); } } else if (or.getBucket() != null) { StorageService storageService; Bucket bucket; storageService = (StorageService) getWebApplicationContext().getBean(BEAN_STORAGE_SERVICE); if (req.getParameter(PARAMETER_ACL) != null) { // write access control policy Acp acp; CanonicalUser owner; logger.debug("User is providing new ACP for bucket " + or.getBucket()); try { bucket = storageService.loadBucket(or.getBucket()); } catch (DataAccessException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchBucket"); return; } acp = bucket.getAcp(); try { acp.canWrite(requestor); } catch (AccessControlException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } // save owner owner = acp.getOwner(); try { acp = Acp.decode(req.getInputStream()); } catch (IOException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "MalformedACLError"); return; } // maintain owner acp.setOwner(owner); bucket.setAcp(acp); logger.debug("Saving bucket ACP"); logger.debug("ACP: " + Acp.encode(bucket.getAcp())); storageService.storeBucket(bucket); } else { // validate bucket String bucketName = or.getBucket(); if (!isValidBucketName(bucketName)) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidBucketName"); return; } try { bucket = storageService.createBucket(bucketName, requestor); } catch (BucketAlreadyExistsException e) { resp.sendError(HttpServletResponse.SC_CONFLICT, "BucketAlreadyExists"); return; } grantCannedAccessPolicies(req, bucket.getAcp(), requestor); storageService.storeBucket(bucket); } } } catch (NoSuchAlgorithmException e) { e.printStackTrace(); logger.error("Unable to use MD5", e); resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "InternalError"); } catch (IOException e) { e.printStackTrace(); throw e; } finally { if (out != null) { out.close(); out = null; } } } /** * Delete * * @param req * the HttpServletRequest object that contains the request the * client made of the servlet * @param resp * the HttpServletResponse object that contains the response the * servlet returns to the client * @param IOException * if an input or output error occurs while the servlet is * handling the DELETE request * @param ServletException * if the request for the DELETE cannot be handled */ public void methodDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { S3ObjectRequest or; try { or = S3ObjectRequest.create(req, resolvedHost(), (Authenticator) getWebApplicationContext().getBean(BEAN_AUTHENTICATOR)); } catch (InvalidAccessKeyIdException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidAccessKeyId"); return; } catch (InvalidSecurityException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidSecurity"); return; } catch (RequestTimeTooSkewedException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "RequestTimeTooSkewed"); return; } catch (SignatureDoesNotMatchException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "SignatureDoesNotMatch"); return; } catch (AuthenticatorException e) { e.printStackTrace(); resp.sendError(HttpServletResponse.SC_FORBIDDEN, "InvalidSecurity"); return; } logger.debug("S3ObjectRequest: " + or); CanonicalUser requestor = or.getRequestor(); if (or.getKey() != null) { Bucket bucket; S3Object s3Object; StorageService storageService; storageService = (StorageService) getWebApplicationContext().getBean(BEAN_STORAGE_SERVICE); // make sure requester can "WRITE" to the bucket try { bucket = storageService.loadBucket(or.getBucket()); bucket.canWrite(requestor); } catch (AccessControlException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } catch (DataAccessException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchBucket"); return; } try { s3Object = storageService.load(bucket.getName(), or.getKey()); } catch (DataRetrievalFailureException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchKey"); return; } storageService.remove(s3Object); resp.setStatus(HttpServletResponse.SC_NO_CONTENT); return; } else if (or.getBucket() != null) { StorageService storageService; Bucket bucket; // validate bucket String bucketName = or.getBucket(); storageService = (StorageService) getWebApplicationContext().getBean(BEAN_STORAGE_SERVICE); try { bucket = storageService.loadBucket(bucketName); } catch (DataAccessException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "NoSuchBucket"); return; } if (!requestor.equals(bucket.getAcp().getOwner())) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "AccessDenied"); return; } try { storageService.deleteBucket(bucket); } catch (BucketNotEmptyException e) { resp.sendError(HttpServletResponse.SC_CONFLICT, "BucketNotEmpty"); return; } resp.setStatus(HttpServletResponse.SC_NO_CONTENT); return; } resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); } public static String formatRangeHeaderValue(Range range, long absoluteLength) { StringBuffer buffer = new StringBuffer(); buffer.append("bytes "); buffer.append(range.getStart()); buffer.append("-"); buffer.append(range.getEnd()); buffer.append("/"); buffer.append(absoluteLength); return buffer.toString(); } /** * Validates a bucket name. Bucket names can only contain alphanumeric * characters, underscore (_), period (.), and dash(-). Bucket names must be * between 3 and 255 characters long. * * @param name * The name of the bucket. * @return <code>True</code> if the bucket name is valid, <code>false</code> * otherwise. */ public static boolean isValidBucketName(String name) { // alphanumeric, underscore, period, dash. between 3-255 characters if (name == null) { return false; } char[] chars = name.toCharArray(); if ((chars.length < 3) || (chars.length > 255)) { return false; } for (int i = 0; i < chars.length; i++) { if ((chars[i] >= 'a') && (chars[i] <= 'z')) { return true; } if ((chars[i] >= 'A') && (chars[i] <= 'Z')) { return true; } if ((chars[i] >= '0') && (chars[i] <= '9')) { return true; } if (chars[i] == '_') { return true; } if (chars[i] == '.') { return true; } if (chars[i] == '-') { return true; } } return false; } /** * Validates a key. A key can be at most 1024 bytes long. * * @param name * The key. * @return <code>True</code> if the key is valid, <code>false</code> * otherwise. */ public static boolean isValidKey(String name) { if (name.length() > 1024) { return false; } return true; } /** * Grant the canned access policies for buckets or objects as part of a * <code>PUT</code> operation. The canned access policies are specified in * the Amazon S3 Developer Guide. * * @param acp * The Access Control Policy to grant the canned access policies * to. * @param owner * The principal making the request who is the owner of the * resource. */ public static void grantCannedAccessPolicies(HttpServletRequest req, Acp acp, CanonicalUser owner) { String xAmzAcl; xAmzAcl = req.getHeader(HEADER_X_AMZ_ACL); if ((xAmzAcl == null) || (xAmzAcl.equals(ACL_PRIVATE))) { acp.grant(owner, ResourcePermission.ACTION_FULL_CONTROL); } else if (xAmzAcl.equals(ACL_PUBLIC_READ)) { acp.grant(owner, ResourcePermission.ACTION_FULL_CONTROL); acp.grant(AllUsersGroup.getInstance(), ResourcePermission.ACTION_READ); } else if (xAmzAcl.equals(ACL_PUBLIC_READ_WRITE)) { acp.grant(owner, ResourcePermission.ACTION_FULL_CONTROL); acp.grant(AllUsersGroup.getInstance(), ResourcePermission.ACTION_READ); acp.grant(AllUsersGroup.getInstance(), ResourcePermission.ACTION_WRITE); } else if (xAmzAcl.equals(ACL_AUTHENTICATED_READ)) { acp.grant(owner, ResourcePermission.ACTION_FULL_CONTROL); acp.grant(AuthenticatedUsersGroup.getInstance(), ResourcePermission.ACTION_READ); } } /** * Resolves the configured host name, replacing any tokens in the configured * host name value. * * @return The configured host name after any tokens have been replaced. * @see #CONFIG_HOST * @see #CONFIG_HOST_TOKEN_RESOLVED_LOCAL_HOST */ public String resolvedHost() { String configHost; configHost = configuration.getString(CONFIG_HOST); logger.debug("configHost: " + configHost); if (configHost.indexOf(CONFIG_HOST_TOKEN_RESOLVED_LOCAL_HOST) >= 0) { InetAddress localHost; String resolvedLocalHost = "localhost"; try { localHost = InetAddress.getLocalHost(); resolvedLocalHost = localHost.getCanonicalHostName(); } catch (UnknownHostException e) { logger.fatal("Unable to resolve local host", e); } configHost = configHost.replace(CONFIG_HOST_TOKEN_RESOLVED_LOCAL_HOST, resolvedLocalHost); } return configHost; } }