/*
* Copyright (C) 2011 Citrix Systems, Inc. All rights reserved.
*
* 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.cloud.bridge.service.controller.s3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.DatatypeConverter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.XMLStreamException;
import org.apache.log4j.Logger;
import org.json.simple.parser.ParseException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.amazon.s3.GetBucketAccessControlPolicyResponse;
import com.amazon.s3.ListAllMyBucketsResponse;
import com.amazon.s3.ListBucketResponse;
import com.cloud.bridge.io.MTOMAwareResultStreamWriter;
import com.cloud.bridge.model.SAcl;
import com.cloud.bridge.model.SBucket;
import com.cloud.bridge.model.SHost;
import com.cloud.bridge.persist.PersistContext;
import com.cloud.bridge.persist.dao.BucketPolicyDao;
import com.cloud.bridge.persist.dao.MultipartLoadDao;
import com.cloud.bridge.persist.dao.SAclDao;
import com.cloud.bridge.persist.dao.SBucketDao;
import com.cloud.bridge.service.S3Constants;
import com.cloud.bridge.service.S3RestServlet;
import com.cloud.bridge.service.controller.s3.ServiceProvider;
import com.cloud.bridge.service.UserContext;
import com.cloud.bridge.service.core.s3.S3AccessControlList;
import com.cloud.bridge.service.core.s3.S3AccessControlPolicy;
import com.cloud.bridge.service.core.s3.S3AuthParams;
import com.cloud.bridge.service.core.s3.S3BucketAdapter;
import com.cloud.bridge.service.core.s3.S3BucketPolicy;
import com.cloud.bridge.service.core.s3.S3CanonicalUser;
import com.cloud.bridge.service.core.s3.S3CreateBucketConfiguration;
import com.cloud.bridge.service.core.s3.S3CreateBucketRequest;
import com.cloud.bridge.service.core.s3.S3CreateBucketResponse;
import com.cloud.bridge.service.core.s3.S3DeleteBucketRequest;
import com.cloud.bridge.service.core.s3.S3DeleteObjectRequest;
import com.cloud.bridge.service.core.s3.S3Engine;
import com.cloud.bridge.service.core.s3.S3GetBucketAccessControlPolicyRequest;
import com.cloud.bridge.service.core.s3.S3Grant;
import com.cloud.bridge.service.core.s3.S3ListAllMyBucketsEntry;
import com.cloud.bridge.service.core.s3.S3ListAllMyBucketsRequest;
import com.cloud.bridge.service.core.s3.S3ListAllMyBucketsResponse;
import com.cloud.bridge.service.core.s3.S3ListBucketObjectEntry;
import com.cloud.bridge.service.core.s3.S3ListBucketRequest;
import com.cloud.bridge.service.core.s3.S3ListBucketResponse;
import com.cloud.bridge.service.core.s3.S3MetaDataEntry;
import com.cloud.bridge.service.core.s3.S3MultipartUpload;
import com.cloud.bridge.service.core.s3.S3PolicyContext;
import com.cloud.bridge.service.core.s3.S3PutObjectRequest;
import com.cloud.bridge.service.core.s3.S3Response;
import com.cloud.bridge.service.core.s3.S3SetBucketAccessControlPolicyRequest;
import com.cloud.bridge.service.core.s3.S3BucketPolicy.PolicyAccess;
import com.cloud.bridge.service.core.s3.S3PolicyAction.PolicyActions;
import com.cloud.bridge.service.core.s3.S3PolicyCondition.ConditionKeys;
import com.cloud.bridge.service.exception.InternalErrorException;
import com.cloud.bridge.service.exception.InvalidBucketName;
import com.cloud.bridge.service.exception.InvalidRequestContentException;
import com.cloud.bridge.service.exception.NetworkIOException;
import com.cloud.bridge.service.exception.NoSuchObjectException;
import com.cloud.bridge.service.exception.ObjectAlreadyExistsException;
import com.cloud.bridge.service.exception.OutOfServiceException;
import com.cloud.bridge.service.exception.PermissionDeniedException;
import com.cloud.bridge.util.Converter;
import com.cloud.bridge.util.DateHelper;
import com.cloud.bridge.util.HeaderParam;
import com.cloud.bridge.util.PolicyParser;
import com.cloud.bridge.util.StringHelper;
import com.cloud.bridge.util.OrderedPair;
import com.cloud.bridge.util.Triple;
import com.cloud.bridge.util.XSerializer;
import com.cloud.bridge.util.XSerializerXmlAdapter;
import com.cloud.bridge.util.XmlHelper;
/**
* @author Kelven Yang, John Zucker
*/
public class S3BucketAction implements ServletAction {
protected final static Logger logger = Logger.getLogger(S3BucketAction.class);
private DocumentBuilderFactory dbf = null;
public S3BucketAction() {
dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware( true );
}
public void execute(HttpServletRequest request, HttpServletResponse response)
throws IOException, XMLStreamException
{
String method = request.getMethod();
String queryString = request.getQueryString();
if ( method.equalsIgnoreCase("PUT"))
{
if ( queryString != null && queryString.length() > 0 )
{
if ( queryString.startsWith("acl")) {
executePutBucketAcl(request, response);
return;
}
else if (queryString.startsWith("versioning")) {
executePutBucketVersioning(request, response);
return;
}
else if (queryString.startsWith("policy")) {
executePutBucketPolicy(request, response);
return;
}
else if (queryString.startsWith("logging")) {
executePutBucketLogging(request, response);
return;
}
else if (queryString.startsWith("website")) {
executePutBucketWebsite(request, response);
return;
}
}
executePutBucket(request, response);
}
else if(method.equalsIgnoreCase("GET") || method.equalsIgnoreCase("HEAD"))
{
if (queryString != null && queryString.length() > 0)
{
if ( queryString.startsWith("acl")) {
executeGetBucketAcl(request, response);
return;
}
else if (queryString.startsWith("versioning")) {
executeGetBucketVersioning(request, response);
return;
}
else if (queryString.contains("versions")) {
executeGetBucketObjectVersions(request, response);
return;
}
else if (queryString.startsWith("location")) {
executeGetBucketLocation(request, response);
return;
}
else if (queryString.startsWith("uploads")) {
executeListMultipartUploads(request, response);
return;
}
else if (queryString.startsWith("policy")) {
executeGetBucketPolicy(request, response);
return;
}
else if (queryString.startsWith("logging")) {
executeGetBucketLogging(request, response);
return;
}
else if (queryString.startsWith("website")) {
executeGetBucketWebsite(request, response);
return;
}
}
String bucketAtr = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
if ( bucketAtr.equals( "/" ))
executeGetAllBuckets(request, response);
else executeGetBucket(request, response);
}
else if (method.equalsIgnoreCase("DELETE"))
{
if (queryString != null && queryString.length() > 0)
{
if ( queryString.startsWith("policy")) {
executeDeleteBucketPolicy(request, response);
return;
}
else if (queryString.startsWith("website")) {
executeDeleteBucketWebsite(request, response);
return;
}
}
executeDeleteBucket(request, response);
}
else if ( (method.equalsIgnoreCase("POST")) && (queryString.equalsIgnoreCase("delete")) )
{
executeMultiObjectDelete(request, response);
}
else throw new IllegalArgumentException("Unsupported method in REST request");
}
private void executeMultiObjectDelete(HttpServletRequest request, HttpServletResponse response) throws IOException{
int contentLength = request.getContentLength();
StringBuffer xmlDeleteResponse = null;
boolean quite = true;
if(contentLength > 0)
{
InputStream is = null;
String versionID =null;
try {
is = request.getInputStream();
String xml = StringHelper.stringFromStream(is);
String elements[] = {"Key","VersionId"};
Document doc = XmlHelper.parse(xml);
Node node = XmlHelper.getRootNode(doc);
if(node == null) {
System.out.println("Invalid XML document, no root element");
return;
}
xmlDeleteResponse = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<DeleteResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">");
String bucket = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
S3DeleteObjectRequest engineRequest = new S3DeleteObjectRequest();
engineRequest.setBucketName( bucket );
is.close();
doc.getDocumentElement().normalize();
NodeList qList = doc.getElementsByTagName("Quiet");
if (qList.getLength() == 1 ) {
Node qNode= qList.item(0);
if ( qNode.getFirstChild().getNodeValue().equalsIgnoreCase("true") == false )
quite = false;
logger.debug("Quite value :" + qNode.getFirstChild().getNodeValue());
}
NodeList objList = doc.getElementsByTagName("Object");
for (int i = 0; i < objList.getLength(); i++) {
Node key = objList.item(i);
NodeList key_data = key.getChildNodes();
if (key.getNodeType() == Node.ELEMENT_NODE) {
Element eElement = (Element) key;
String key_name = getTagValue(elements[0], eElement);
engineRequest.setBucketName(bucket);
engineRequest.setKey(key_name);
if (key_data.getLength() == 2) {
versionID = getTagValue(elements[1], eElement);
engineRequest.setVersion(versionID);
}
S3Response engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest( engineRequest );
int resultCode = engineResponse.getResultCode();
String resutlDesc = engineResponse.getResultDescription();
if(resultCode == 204) {
if (quite) { // show response depending on quite/verbose
xmlDeleteResponse.append("<Deleted><Key>"+key_name+"</Key>");
if (resutlDesc != null)
xmlDeleteResponse.append(resutlDesc);
xmlDeleteResponse.append("</Deleted>");
}
}
else {
logger.debug("Error in delete ::" + key_name + " eng response:: " + engineResponse.getResultDescription());
xmlDeleteResponse.append("<Error><Key>"+key_name+"</Key>" );
if (resutlDesc != null)
xmlDeleteResponse.append(resutlDesc);
xmlDeleteResponse.append("</Error>");
}
}
}
String version = engineRequest.getVersion();
if (null != version) response.addHeader( "x-amz-version-id", version );
} catch (IOException e) {
logger.error("Unable to read request data due to " + e.getMessage(), e);
throw new NetworkIOException(e);
} finally {
if(is != null) is.close();
}
xmlDeleteResponse.append("</DeleteResult>");
}
response.setStatus(200);
response.setContentType("text/xml; charset=UTF-8");
S3RestServlet.endResponse(response, xmlDeleteResponse.toString());
}
private String getTagValue(String sTag, Element eElement) {
NodeList nlList = eElement.getElementsByTagName(sTag).item(0).getChildNodes();
Node nValue = (Node) nlList.item(0);
return nValue.getNodeValue();
}
/**
* In order to support a policy on the "s3:CreateBucket" action we must be able to set and get
* policies before a bucket is actually created.
*
* @param request
* @param response
* @throws IOException
*/
private void executePutBucketPolicy(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
String policy = streamToString( request.getInputStream());
// [A] Is there an owner of an existing policy or bucket?
BucketPolicyDao policyDao = new BucketPolicyDao();
SBucketDao bucketDao = new SBucketDao();
SBucket bucket = bucketDao.getByName( bucketName );
String owner = null;
if ( null != bucket )
{
owner = bucket.getOwnerCanonicalId();
}
else
{ try {
owner = policyDao.getPolicyOwner( bucketName );
}
catch( Exception e ) {}
}
// [B] "The bucket owner by default has permissions to attach bucket policies to their buckets using PUT Bucket policy."
// -> the bucket owner may want to restrict the IP address from where this can be executed
String client = UserContext.current().getCanonicalUserId();
S3PolicyContext context = new S3PolicyContext( PolicyActions.PutBucketPolicy, bucketName );
switch( S3Engine.verifyPolicy( context )) {
case ALLOW:
break;
case DEFAULT_DENY:
if (null != owner && !client.equals( owner )) {
response.setStatus(405);
return;
}
break;
case DENY:
response.setStatus(403);
return;
}
// [B] Place the policy into the database over writting an existing policy
try {
// -> first make sure that the policy is valid by parsing it
PolicyParser parser = new PolicyParser();
S3BucketPolicy sbp = parser.parse( policy, bucketName );
policyDao.deletePolicy( bucketName );
if (null != policy && !policy.isEmpty()) policyDao.addPolicy( bucketName, client, policy );
if (null != sbp) ServiceProvider.getInstance().setBucketPolicy( bucketName, sbp );
response.setStatus(200);
}
catch( PermissionDeniedException e ) {
logger.error("Put Bucket Policy failed due to " + e.getMessage(), e);
throw e;
}
catch( ParseException e ) {
logger.error("Put Bucket Policy failed due to " + e.getMessage(), e);
throw new PermissionDeniedException( e.toString());
}
catch( Exception e ) {
logger.error("Put Bucket Policy failed due to " + e.getMessage(), e);
response.setStatus(500);
}
}
private void executeGetBucketPolicy(HttpServletRequest request, HttpServletResponse response)
{
String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
// [A] Is there an owner of an existing policy or bucket?
BucketPolicyDao policyDao = new BucketPolicyDao();
SBucketDao bucketDao = new SBucketDao();
SBucket bucket = bucketDao.getByName( bucketName );
String owner = null;
if ( null != bucket )
{
owner = bucket.getOwnerCanonicalId();
}
else
{ try {
owner = policyDao.getPolicyOwner( bucketName );
}
catch( Exception e ) {}
}
// [B] "The bucket owner by default has permissions to retrieve bucket policies using GET Bucket policy."
// -> the bucket owner may want to restrict the IP address from where this can be executed
String client = UserContext.current().getCanonicalUserId();
S3PolicyContext context = new S3PolicyContext( PolicyActions.GetBucketPolicy, bucketName );
switch( S3Engine.verifyPolicy( context )) {
case ALLOW:
break;
case DEFAULT_DENY:
if (null != owner && !client.equals( owner )) {
response.setStatus(405);
return;
}
break;
case DENY:
response.setStatus(403);
return;
}
// [B] Pull the policy from the database if one exists
try {
String policy = policyDao.getPolicy( bucketName );
if ( null == policy ) {
response.setStatus(404);
}
else {
response.setStatus(200);
response.setContentType("application/json");
S3RestServlet.endResponse(response, policy);
}
}
catch( Exception e ) {
logger.error("Get Bucket Policy failed due to " + e.getMessage(), e);
response.setStatus(500);
}
}
private void executeDeleteBucketPolicy(HttpServletRequest request, HttpServletResponse response)
{
String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
SBucketDao bucketDao = new SBucketDao();
SBucket bucket = bucketDao.getByName( bucketName );
if (bucket != null)
{
String client = UserContext.current().getCanonicalUserId();
if (!client.equals( bucket.getOwnerCanonicalId())) {
response.setStatus(405);
return;
}
}
try {
BucketPolicyDao policyDao = new BucketPolicyDao();
String policy = policyDao.getPolicy( bucketName );
if ( null == policy ) {
response.setStatus(204);
}
else {
ServiceProvider.getInstance().deleteBucketPolicy( bucketName );
policyDao.deletePolicy( bucketName );
response.setStatus(200);
}
}
catch( Exception e ) {
logger.error("Delete Bucket Policy failed due to " + e.getMessage(), e);
response.setStatus(500);
}
}
public void executeGetAllBuckets(HttpServletRequest request, HttpServletResponse response)
throws IOException, XMLStreamException
{
Calendar cal = Calendar.getInstance();
cal.set( 1970, 1, 1 );
S3ListAllMyBucketsRequest engineRequest = new S3ListAllMyBucketsRequest();
engineRequest.setAccessKey(UserContext.current().getAccessKey());
engineRequest.setRequestTimestamp( cal );
engineRequest.setSignature( "" );
S3ListAllMyBucketsResponse engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest(engineRequest);
// To allow the all buckets list to be serialized via Axiom classes
ListAllMyBucketsResponse allBuckets = S3SerializableServiceImplementation.toListAllMyBucketsResponse( engineResponse );
OutputStream outputStream = response.getOutputStream();
response.setStatus(200);
response.setContentType("application/xml");
// The content-type literally should be "application/xml; charset=UTF-8"
// but any compliant JVM supplies utf-8 by default
// MTOMAwareResultStreamWriter resultWriter = new MTOMAwareResultStreamWriter ("ListAllMyBucketsResult", outputStream );
// resultWriter.startWrite();
// resultWriter.writeout(allBuckets);
// resultWriter.stopWrite();
StringBuffer xml = new StringBuffer();
xml.append( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
xml.append("<ListAllMyBucketsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">");
xml.append("<Owner><ID>");
xml.append(engineResponse.getOwner().getID()).append("</ID>");
xml.append("<DisplayName>").append(engineResponse.getOwner().getDisplayName()).append("</DisplayName>");
xml.append("</Owner>").append("<Buckets>");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
for (S3ListAllMyBucketsEntry entry :engineResponse.getBuckets()) {
xml.append("<Bucket>").append("<Name>").append(entry.getName()).append("</Name>");
xml.append("<CreationDate>").append(sdf.format(entry.getCreationDate().getTime())).append("</CreationDate>");
xml.append("</Bucket>");
}
xml.append("</Buckets>").append("</ListAllMyBucketsResult>");
response.setStatus(200);
response.setContentType("text/xml; charset=UTF-8");
S3RestServlet.endResponse(response, xml.toString());
}
public void executeGetBucket(HttpServletRequest request, HttpServletResponse response)
throws IOException, XMLStreamException
{
S3ListBucketRequest engineRequest = new S3ListBucketRequest();
engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));
engineRequest.setDelimiter(request.getParameter("delimiter"));
engineRequest.setMarker(request.getParameter("marker"));
engineRequest.setPrefix(request.getParameter("prefix"));
int maxKeys = Converter.toInt(request.getParameter("max-keys"), 1000);
engineRequest.setMaxKeys(maxKeys);
try {
S3ListBucketResponse engineResponse = ServiceProvider.getInstance().getS3Engine().listBucketContents( engineRequest, false );
// To allow the all list buckets result to be serialized via Axiom classes
ListBucketResponse oneBucket = S3SerializableServiceImplementation.toListBucketResponse( engineResponse );
OutputStream outputStream = response.getOutputStream();
response.setStatus(200);
response.setContentType("application/xml");
// The content-type literally should be "application/xml; charset=UTF-8"
// but any compliant JVM supplies utf-8 by default;
MTOMAwareResultStreamWriter resultWriter = new MTOMAwareResultStreamWriter ("ListBucketResult", outputStream );
resultWriter.startWrite();
resultWriter.writeout(oneBucket);
resultWriter.stopWrite();
} catch (NoSuchObjectException nsoe) {
response.setStatus(404);
response.setContentType("application/xml");
StringBuffer xmlError = new StringBuffer();
xmlError.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
.append("<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message>")
.append("<BucketName>").append((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY))
.append("</BucketName>")
.append("<RequestId>1DEADBEEF9</RequestId>") //TODO
.append("<HostId>abCdeFgHiJ1k2LmN3op4q56r7st89</HostId>") //TODO
.append("</Error>");
S3RestServlet.endResponse(response, xmlError.toString());
}
}
public void executeGetBucketAcl(HttpServletRequest request, HttpServletResponse response)
throws IOException, XMLStreamException
{
S3GetBucketAccessControlPolicyRequest engineRequest = new S3GetBucketAccessControlPolicyRequest();
Calendar cal = Calendar.getInstance();
cal.set( 1970, 1, 1 );
engineRequest.setAccessKey(UserContext.current().getAccessKey());
engineRequest.setRequestTimestamp( cal );
engineRequest.setSignature( "" ); // TODO - Consider providing signature in a future release which allows additional user description
engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));
S3AccessControlPolicy engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest(engineRequest);
// To allow the bucket acl policy result to be serialized via Axiom classes
GetBucketAccessControlPolicyResponse onePolicy = S3SerializableServiceImplementation.toGetBucketAccessControlPolicyResponse( engineResponse );
OutputStream outputStream = response.getOutputStream();
response.setStatus(200);
response.setContentType("application/xml");
// The content-type literally should be "application/xml; charset=UTF-8"
// but any compliant JVM supplies utf-8 by default;
MTOMAwareResultStreamWriter resultWriter = new MTOMAwareResultStreamWriter ("GetBucketAccessControlPolicyResult", outputStream );
resultWriter.startWrite();
resultWriter.writeout(onePolicy);
resultWriter.stopWrite();
}
public void executeGetBucketVersioning(HttpServletRequest request, HttpServletResponse response) throws IOException
{
// [A] Does the bucket exist?
String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
String versioningStatus = null;
if (null == bucketName) {
logger.error( "executeGetBucketVersioning - no bucket name given" );
response.setStatus( 400 );
return;
}
SBucketDao bucketDao = new SBucketDao();
SBucket sbucket = bucketDao.getByName( bucketName );
if (sbucket == null) {
response.setStatus( 404 );
return;
}
// [B] The owner may want to restrict the IP address at which this can be performed
String client = UserContext.current().getCanonicalUserId();
if (!client.equals( sbucket.getOwnerCanonicalId()))
throw new PermissionDeniedException( "Access Denied - only the owner can read bucket versioning" );
S3PolicyContext context = new S3PolicyContext( PolicyActions.GetBucketVersioning, bucketName );
if (PolicyAccess.DENY == S3Engine.verifyPolicy( context )) {
response.setStatus(403);
return;
}
// [C]
switch( sbucket.getVersioningStatus()) {
default:
case 0: versioningStatus = ""; break;
case 1: versioningStatus = "Enabled"; break;
case 2: versioningStatus = "Suspended"; break;
}
StringBuffer xml = new StringBuffer();
xml.append( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
xml.append( "<VersioningConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">" );
if (0 < versioningStatus.length()) xml.append( "<Status>" ).append( versioningStatus ).append( "</Status>" );
xml.append( "</VersioningConfiguration>" );
response.setStatus(200);
response.setContentType("text/xml; charset=UTF-8");
S3RestServlet.endResponse(response, xml.toString());
}
public void executeGetBucketObjectVersions(HttpServletRequest request, HttpServletResponse response) throws IOException
{
S3ListBucketRequest engineRequest = new S3ListBucketRequest();
String keyMarker = request.getParameter("key-marker");
String versionIdMarker = request.getParameter("version-id-marker");
engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));
engineRequest.setDelimiter(request.getParameter("delimiter"));
engineRequest.setMarker( keyMarker );
engineRequest.setPrefix(request.getParameter("prefix"));
engineRequest.setVersionIdMarker( versionIdMarker );
int maxKeys = Converter.toInt(request.getParameter("max-keys"), 1000);
engineRequest.setMaxKeys(maxKeys);
S3ListBucketResponse engineResponse = ServiceProvider.getInstance().getS3Engine().listBucketContents( engineRequest, true );
// -> the SOAP version produces different XML
StringBuffer xml = new StringBuffer();
xml.append( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
xml.append( "<ListVersionsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">" );
xml.append( "<Name>" ).append( engineResponse.getBucketName()).append( "</Name>" );
if ( null == keyMarker )
xml.append( "<KeyMarker/>" );
else xml.append( "<KeyMarker>" ).append( keyMarker ).append( "</KeyMarker" );
if ( null == versionIdMarker )
xml.append( "<VersionIdMarker/>" );
else xml.append( "<VersionIdMarker>" ).append( keyMarker ).append( "</VersionIdMarker" );
xml.append( "<MaxKeys>" ).append( engineResponse.getMaxKeys()).append( "</MaxKeys>" );
xml.append( "<IsTruncated>" ).append( engineResponse.isTruncated()).append( "</IsTruncated>" );
S3ListBucketObjectEntry[] versions = engineResponse.getContents();
for( int i=0; null != versions && i < versions.length; i++ )
{
S3CanonicalUser owner = versions[i].getOwner();
boolean isDeletionMarker = versions[i].getIsDeletionMarker();
String displayName = owner.getDisplayName();
String id = owner.getID();
if ( isDeletionMarker )
{
xml.append( "<DeleteMarker>" );
xml.append( "<Key>" ).append( versions[i].getKey()).append( "</Key>" );
xml.append( "<VersionId>" ).append( versions[i].getVersion()).append( "</VersionId>" );
xml.append( "<IsLatest>" ).append( versions[i].getIsLatest()).append( "</IsLatest>" );
xml.append( "<LastModified>" ).append( DatatypeConverter.printDateTime( versions[i].getLastModified())).append( "</LastModified>" );
}
else
{ xml.append( "<Version>" );
xml.append( "<Key>" ).append( versions[i].getKey()).append( "</Key>" );
xml.append( "<VersionId>" ).append( versions[i].getVersion()).append( "</VersionId>" );
xml.append( "<IsLatest>" ).append( versions[i].getIsLatest()).append( "</IsLatest>" );
xml.append( "<LastModified>" ).append( DatatypeConverter.printDateTime( versions[i].getLastModified())).append( "</LastModified>" );
xml.append( "<ETag>" ).append( versions[i].getETag()).append( "</ETag>" );
xml.append( "<Size>" ).append( versions[i].getSize()).append( "</Size>" );
xml.append( "<StorageClass>" ).append( versions[i].getStorageClass()).append( "</StorageClass>" );
}
xml.append( "<Owner>" );
xml.append( "<ID>" ).append( id ).append( "</ID>" );
if ( null == displayName )
xml.append( "<DisplayName/>" );
else xml.append( "<DisplayName>" ).append( owner.getDisplayName()).append( "</DisplayName>" );
xml.append( "</Owner>" );
if ( isDeletionMarker )
xml.append( "</DeleteMarker>" );
else xml.append( "</Version>" );
}
xml.append( "</ListVersionsResult>" );
response.setStatus(200);
response.setContentType("text/xml; charset=UTF-8");
S3RestServlet.endResponse(response, xml.toString());
}
public void executeGetBucketLogging(HttpServletRequest request, HttpServletResponse response) throws IOException {
// TODO -- Review this in future. Currently this is a beta feature of S3
response.setStatus(405);
}
public void executeGetBucketLocation(HttpServletRequest request, HttpServletResponse response) throws IOException {
// TODO - This is a fakery! We don't actually store location in backend
StringBuffer xml = new StringBuffer();
xml.append( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
xml.append( "<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">" );
// This is the real fakery
xml.append( "us-west-2" );
xml.append( "</LocationConstraint>" );
response.setStatus(200);
response.setContentType("text/xml; charset=UTF-8");
S3RestServlet.endResponse(response, xml.toString());
}
public void executeGetBucketWebsite(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setStatus(405);
}
public void executeDeleteBucketWebsite(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setStatus(405);
}
public void executePutBucket(HttpServletRequest request, HttpServletResponse response) throws IOException
{
int contentLength = request.getContentLength();
Object objectInContent = null;
if(contentLength > 0)
{
InputStream is = null;
try {
is = request.getInputStream();
String xml = StringHelper.stringFromStream(is);
Class.forName("com.cloud.bridge.service.core.s3.S3CreateBucketConfiguration");
XSerializer serializer = new XSerializer(new XSerializerXmlAdapter());
objectInContent = serializer.serializeFrom(xml);
if(objectInContent != null && !(objectInContent instanceof S3CreateBucketConfiguration)) {
throw new InvalidRequestContentException("Invalid request content in create-bucket: " + xml);
}
is.close();
} catch (IOException e) {
logger.error("Unable to read request data due to " + e.getMessage(), e);
throw new NetworkIOException(e);
} catch (ClassNotFoundException e) {
logger.error("In a normal world this should never never happen:" + e.getMessage(), e);
throw new RuntimeException("A required class was not found in the classpath:" + e.getMessage());
}
finally {
if(is != null) is.close();
}
}
S3CreateBucketRequest engineRequest = new S3CreateBucketRequest();
engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));
engineRequest.setConfig((S3CreateBucketConfiguration)objectInContent);
try {
S3CreateBucketResponse engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest(engineRequest);
response.addHeader("Location", "/" + engineResponse.getBucketName());
response.setContentLength(0);
response.setStatus(200);
response.flushBuffer();
} catch (ObjectAlreadyExistsException oaee) {
response.setStatus(409);
String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <Error><Code>OperationAborted</Code><Message>A conflicting conditional operation is currently in progress against this resource. Please try again..</Message>";
response.setContentType("text/xml; charset=UTF-8");
S3RestServlet.endResponse(response, xml.toString());
}
}
public void executePutBucketAcl(HttpServletRequest request, HttpServletResponse response) throws IOException
{
// [A] Determine that there is an applicable bucket which might have an ACL set
String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
SBucketDao bucketDao = new SBucketDao();
SBucket bucket = bucketDao.getByName( bucketName );
String owner = null;
if ( null != bucket )
owner = bucket.getOwnerCanonicalId();
if (null == owner)
{
logger.error( "ACL update failed since " + bucketName + " does not exist" );
throw new IOException("ACL update failed");
}
// [B] Obtain the grant request which applies to the acl request string. This latter is supplied as the value of the x-amz-acl header.
S3SetBucketAccessControlPolicyRequest engineRequest = new S3SetBucketAccessControlPolicyRequest();
S3Grant grantRequest = new S3Grant();
S3AccessControlList aclRequest = new S3AccessControlList();
String aclRequestString = request.getHeader("x-amz-acl");
OrderedPair <Integer,Integer> accessControlsForBucketOwner = SAcl.getCannedAccessControls(aclRequestString,"SBucket");
grantRequest.setPermission(accessControlsForBucketOwner.getFirst());
grantRequest.setGrantee(accessControlsForBucketOwner.getSecond());
grantRequest.setCanonicalUserID(owner);
aclRequest.addGrant(grantRequest);
engineRequest.setAcl(aclRequest);
engineRequest.setBucketName(bucketName);
// [C] Allow an S3Engine to handle the S3SetBucketAccessControlPolicyRequest
S3Response engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest(engineRequest);
response.setStatus( engineResponse.getResultCode());
}
public void executePutBucketVersioning(HttpServletRequest request, HttpServletResponse response) throws IOException
{
String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
String versioningStatus = null;
Node item = null;
if (null == bucketName) {
logger.error( "executePutBucketVersioning - no bucket name given" );
response.setStatus( 400 );
return;
}
// -> is the XML as defined?
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document restXML = db.parse( request.getInputStream());
NodeList match = S3RestServlet.getElement( restXML, "http://s3.amazonaws.com/doc/2006-03-01/", "Status" );
if ( 0 < match.getLength())
{
item = match.item(0);
versioningStatus = new String( item.getFirstChild().getNodeValue());
}
else
{ logger.error( "executePutBucketVersioning - cannot find Status tag in XML body" );
response.setStatus( 400 );
return;
}
}
catch( Exception e ) {
logger.error( "executePutBucketVersioning - failed to parse XML due to " + e.getMessage(), e);
response.setStatus(400);
return;
}
try {
// Irrespective of what the ACLs say only the owner can turn on versioning on a bucket.
// The bucket owner may want to restrict the IP address from which this can occur.
SBucketDao bucketDao = new SBucketDao();
SBucket sbucket = bucketDao.getByName( bucketName );
String client = UserContext.current().getCanonicalUserId();
if (!client.equals( sbucket.getOwnerCanonicalId()))
throw new PermissionDeniedException( "Access Denied - only the owner can turn on versioing on a bucket" );
S3PolicyContext context = new S3PolicyContext( PolicyActions.PutBucketVersioning, bucketName );
if (PolicyAccess.DENY == S3Engine.verifyPolicy( context )) {
response.setStatus(403);
return;
}
if (versioningStatus.equalsIgnoreCase( "Enabled" )) sbucket.setVersioningStatus( 1 );
else if (versioningStatus.equalsIgnoreCase( "Suspended")) sbucket.setVersioningStatus( 2 );
else {
logger.error( "executePutBucketVersioning - unknown state: [" + versioningStatus + "]" );
response.setStatus( 400 );
return;
}
bucketDao.update( sbucket );
} catch( PermissionDeniedException e ) {
logger.error( "executePutBucketVersioning - failed due to " + e.getMessage(), e);
throw e;
} catch( Exception e ) {
logger.error( "executePutBucketVersioning - failed due to " + e.getMessage(), e);
response.setStatus(500);
return;
}
response.setStatus(200);
}
public void executePutBucketLogging(HttpServletRequest request, HttpServletResponse response) throws IOException {
// TODO -- Review this in future. Currently this is a S3 beta feature
response.setStatus(501);
}
public void executePutBucketWebsite(HttpServletRequest request, HttpServletResponse response) throws IOException {
// TODO -- LoPri - Undertake checks on Put Bucket Website
// Tested using configuration <Directory /Users/john1/S3-Mount>\nAllowOverride FileInfo AuthConfig Limit...</Directory> in httpd.conf
// Need some way of using AllowOverride to allow use of .htaccess and then pushing .httaccess file to bucket subdirectory of mount point
// Currently has noop effect in the sense that a running apachectl process sees the directory contents without further action
response.setStatus(200);
}
public void executeDeleteBucket(HttpServletRequest request, HttpServletResponse response) throws IOException
{
S3DeleteBucketRequest engineRequest = new S3DeleteBucketRequest();
engineRequest.setBucketName((String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY));
S3Response engineResponse = ServiceProvider.getInstance().getS3Engine().handleRequest(engineRequest);
response.setStatus(engineResponse.getResultCode());
response.flushBuffer();
}
/**
* Multipart upload is a complex operation with all the options defined by Amazon. Part of the functionality is
* provided by the query done against the database. The CommonPrefixes functionality is done the same way
* as done in the listBucketContents function (i.e., by iterating though the list to decide which output
* element each key is placed).
*
* @param request
* @param response
* @throws IOException
*/
public void executeListMultipartUploads(HttpServletRequest request, HttpServletResponse response) throws IOException
{
// [A] Obtain parameters and do basic bucket verification
String bucketName = (String)request.getAttribute(S3Constants.BUCKET_ATTR_KEY);
String delimiter = request.getParameter("delimiter");
String keyMarker = request.getParameter("key-marker");
String prefix = request.getParameter("prefix");
int maxUploads = 1000;
int nextUploadId = 0;
String nextKey = null;
boolean isTruncated = false;
S3MultipartUpload[] uploads = null;
S3MultipartUpload onePart = null;
String temp = request.getParameter("max-uploads");
if (null != temp) {
maxUploads = Integer.parseInt( temp );
if (maxUploads > 1000 || maxUploads < 0) maxUploads = 1000;
}
// -> upload-id-marker is ignored unless key-marker is also specified
String uploadIdMarker = request.getParameter("upload-id-marker");
if (null == keyMarker) uploadIdMarker = null;
// -> does the bucket exist, we may need it to verify access permissions
SBucketDao bucketDao = new SBucketDao();
SBucket bucket = bucketDao.getByName(bucketName);
if (bucket == null) {
logger.error( "listMultipartUpload failed since " + bucketName + " does not exist" );
response.setStatus(404);
return;
}
S3PolicyContext context = new S3PolicyContext( PolicyActions.ListBucketMultipartUploads, bucketName );
context.setEvalParam( ConditionKeys.Prefix, prefix );
context.setEvalParam( ConditionKeys.Delimiter, delimiter );
S3Engine.verifyAccess( context, "SBucket", bucket.getId(), SAcl.PERMISSION_READ );
// [B] Query the multipart table to get the list of current uploads
try {
MultipartLoadDao uploadDao = new MultipartLoadDao();
OrderedPair<S3MultipartUpload[],Boolean> result = uploadDao.getInitiatedUploads( bucketName, maxUploads, prefix, keyMarker, uploadIdMarker );
uploads = result.getFirst();
isTruncated = result.getSecond().booleanValue();
}
catch( Exception e ) {
logger.error("List Multipart Uploads failed due to " + e.getMessage(), e);
response.setStatus(500);
}
StringBuffer xml = new StringBuffer();
xml.append( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
xml.append( "<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">" );
xml.append( "<Bucket>" ).append( bucketName ).append( "</Bucket>" );
xml.append( "<KeyMarker>").append((null == keyMarker ? "" : keyMarker)).append( "</KeyMarker>" );
xml.append( "<UploadIdMarker>").append((null == uploadIdMarker ? "" : uploadIdMarker)).append( "</UploadIdMarker>" );
// [C] Construct the contents of the <Upload> element
StringBuffer partsList = new StringBuffer();
for( int i=0; i < uploads.length; i++ )
{
onePart = uploads[i];
if (null == onePart) break;
if (delimiter != null && !delimiter.isEmpty())
{
// -> is this available only in the CommonPrefixes element?
if (StringHelper.substringInBetween(onePart.getKey(), prefix, delimiter) != null)
continue;
}
nextKey = onePart.getKey();
nextUploadId = onePart.getId();
partsList.append( "<Upload>" );
partsList.append( "<Key>" ).append( nextKey ).append( "</Key>" );
partsList.append( "<UploadId>" ).append( nextUploadId ).append( "</UploadId>" );
partsList.append( "<Initiator>" );
partsList.append( "<ID>" ).append( onePart.getAccessKey()).append( "</ID>" );
partsList.append( "<DisplayName></DisplayName>" );
partsList.append( "</Initiator>" );
partsList.append( "<Owner>" );
partsList.append( "<ID>" ).append( onePart.getAccessKey()).append( "</ID>" );
partsList.append( "<DisplayName></DisplayName>" );
partsList.append( "</Owner>" );
partsList.append( "<StorageClass>STANDARD</StorageClass>" );
partsList.append( "<Initiated>" ).append( DatatypeConverter.printDateTime( onePart.getLastModified())).append( "</Initiated>" );
partsList.append( "</Upload>" );
}
// [D] Construct the contents of the <CommonPrefixes> elements (if any)
for( int i=0; i < uploads.length; i++ )
{
onePart = uploads[i];
if (null == onePart) break;
if (delimiter != null && !delimiter.isEmpty())
{
String subName = StringHelper.substringInBetween(onePart.getKey(), prefix, delimiter);
if (subName != null)
{
partsList.append( "<CommonPrefixes>" );
partsList.append( "<Prefix>" );
if ( prefix != null && prefix.length() > 0 )
partsList.append( prefix + delimiter + subName );
else partsList.append( subName );
partsList.append( "</Prefix>" );
partsList.append( "</CommonPrefixes>" );
}
}
}
// [D] Finish off the response
xml.append( "<NextKeyMarker>" ).append((null == nextKey ? "" : nextKey)).append( "</NextKeyMarker>" );
xml.append( "<NextUploadIdMarker>" ).append((0 == nextUploadId ? "" : nextUploadId)).append( "</NextUploadIdMarker>" );
xml.append( "<MaxUploads>" ).append( maxUploads ).append( "</MaxUploads>" );
xml.append( "<IsTruncated>" ).append( isTruncated ).append( "</IsTruncated>" );
xml.append( partsList.toString());
xml.append( "</ListMultipartUploadsResult>" );
response.setStatus(200);
response.setContentType("text/xml; charset=UTF-8");
S3RestServlet.endResponse(response, xml.toString());
}
private String streamToString( InputStream is ) throws IOException
{
int n = 0;
if ( null != is )
{
Writer writer = new StringWriter();
char[] buffer = new char[1024];
try {
Reader reader = new BufferedReader( new InputStreamReader(is, "UTF-8"));
while ((n = reader.read(buffer)) != -1) writer.write(buffer, 0, n);
}
finally {
is.close();
}
return writer.toString();
}
else return null;
}
}