/*
* 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;
}
}