// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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.Reader; import java.io.StringWriter; import java.io.Writer; import java.text.SimpleDateFormat; import java.util.Calendar; import javax.inject.Inject; 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.BucketPolicyVO; import com.cloud.bridge.model.SAcl; import com.cloud.bridge.model.SAclVO; import com.cloud.bridge.model.SBucketVO; import com.cloud.bridge.persist.dao.BucketPolicyDao; import com.cloud.bridge.persist.dao.MultipartLoadDao; import com.cloud.bridge.persist.dao.SBucketDao; import com.cloud.bridge.service.S3Constants; import com.cloud.bridge.service.S3RestServlet; 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.S3BucketPolicy; import com.cloud.bridge.service.core.s3.S3BucketPolicy.PolicyAccess; 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.S3MultipartUpload; import com.cloud.bridge.service.core.s3.S3PolicyAction.PolicyActions; import com.cloud.bridge.service.core.s3.S3PolicyCondition.ConditionKeys; import com.cloud.bridge.service.core.s3.S3PolicyContext; import com.cloud.bridge.service.core.s3.S3Response; import com.cloud.bridge.service.core.s3.S3SetBucketAccessControlPolicyRequest; 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.PermissionDeniedException; import com.cloud.bridge.util.Converter; import com.cloud.bridge.util.OrderedPair; import com.cloud.bridge.util.PolicyParser; import com.cloud.bridge.util.StringHelper; import com.cloud.bridge.util.XSerializer; import com.cloud.bridge.util.XSerializerXmlAdapter; import com.cloud.bridge.util.XmlHelper; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionLegacy; public class S3BucketAction implements ServletAction { protected final static Logger logger = Logger.getLogger(S3BucketAction.class); @Inject BucketPolicyDao bPolicyDao; @Inject SBucketDao bucketDao; private DocumentBuilderFactory dbf = null; public S3BucketAction() { dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware( true ); } @Override 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("" + ""); 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(""+key_name+""); if (resutlDesc != null) xmlDeleteResponse.append(resutlDesc); xmlDeleteResponse.append(""); } } else { logger.debug("Error in delete ::" + key_name + " eng response:: " + engineResponse.getResultDescription()); xmlDeleteResponse.append(""+key_name+"" ); if (resutlDesc != null) xmlDeleteResponse.append(resutlDesc); xmlDeleteResponse.append(""); } } } 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(""); } 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 = 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? SBucketVO bucket = bucketDao.getByName( bucketName ); String owner = null; if ( null != bucket ) { owner = bucket.getOwnerCanonicalId(); } else { try { owner = bPolicyDao.getByName(bucketName).getOwnerCanonicalID(); } 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; } TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.AWSAPI_DB); // [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 ); bPolicyDao.deletePolicy(bucketName); if (null != policy && !policy.isEmpty()) { BucketPolicyVO bpolicy = new BucketPolicyVO(bucketName, client, policy); bpolicy = bPolicyDao.persist(bpolicy); //policyDao.addPolicy( bucketName, client, policy ); } if (null != sbp) ServiceProvider.getInstance().setBucketPolicy( bucketName, sbp ); response.setStatus(200); txn.commit(); txn.close(); } 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? SBucketVO bucket = bucketDao.getByName(bucketName); String owner = null; if (null != bucket) { owner = bucket.getOwnerCanonicalId(); } else { try { owner = bPolicyDao.getByName(bucketName).getOwnerCanonicalID(); } 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 = bPolicyDao.getByName(bucketName).getPolicy(); 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); SBucketVO bucket = bucketDao.getByName(bucketName); if (bucket != null) { String client = UserContext.current().getCanonicalUserId(); if (!client.equals(bucket.getOwnerCanonicalId())) { response.setStatus(405); return; } } try { String policy = bPolicyDao.getByName(bucketName).getPolicy(); if (null == policy) { response.setStatus(204); } else { ServiceProvider.getInstance().deleteBucketPolicy(bucketName); bPolicyDao.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.append(""); xml.append(""); xml.append(engineResponse.getOwner().getID()).append(""); xml.append("") .append(engineResponse.getOwner().getDisplayName()) .append(""); xml.append("").append(""); SimpleDateFormat sdf = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); for (S3ListAllMyBucketsEntry entry : engineResponse.getBuckets()) { xml.append("").append("").append(entry.getName()) .append(""); xml.append("") .append(sdf.format(entry.getCreationDate().getTime())) .append(""); xml.append(""); } xml.append("").append(""); 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("") .append("NoSuchBucketThe specified bucket does not exist") .append("") .append((String) request .getAttribute(S3Constants.BUCKET_ATTR_KEY)) .append("") .append("1DEADBEEF9") // TODO .append("abCdeFgHiJ1k2LmN3op4q56r7st89") // TODO .append(""); 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; } SBucketVO 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.append( "" ); if (0 < versioningStatus.length()) xml.append( "" ).append( versioningStatus ).append( "" ); xml.append( "" ); 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.append( "" ); xml.append( "" ).append( engineResponse.getBucketName()).append( "" ); if ( null == keyMarker ) xml.append( "" ); else xml.append( "" ).append( keyMarker ).append( "" ); else xml.append( "" ).append( keyMarker ).append( "" ).append( engineResponse.getMaxKeys()).append( "" ); xml.append( "" ).append( engineResponse.isTruncated()).append( "" ); 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( "" ); xml.append( "" ).append( versions[i].getKey()).append( "" ); xml.append( "" ).append( versions[i].getVersion()).append( "" ); xml.append( "" ).append( versions[i].getIsLatest()).append( "" ); xml.append( "" ).append( DatatypeConverter.printDateTime( versions[i].getLastModified())).append( "" ); } else { xml.append( "" ); xml.append( "" ).append( versions[i].getKey()).append( "" ); xml.append( "" ).append( versions[i].getVersion()).append( "" ); xml.append( "" ).append( versions[i].getIsLatest()).append( "" ); xml.append( "" ).append( DatatypeConverter.printDateTime( versions[i].getLastModified())).append( "" ); xml.append( "" ).append( versions[i].getETag()).append( "" ); xml.append( "" ).append( versions[i].getSize()).append( "" ); xml.append( "" ).append( versions[i].getStorageClass()).append( "" ); } xml.append( "" ); xml.append( "" ).append( id ).append( "" ); if ( null == displayName ) xml.append( "" ); else xml.append( "" ).append( owner.getDisplayName()).append( "" ); xml.append( "" ); if ( isDeletionMarker ) xml.append( "" ); else xml.append( "" ); } xml.append( "" ); 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.append( "" ); // This is the real fakery xml.append( "us-west-2" ); xml.append( "" ); 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 = " OperationAbortedA conflicting conditional operation is currently in progress against this resource. Please try again.."; 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); SBucketVO 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 accessControlsForBucketOwner = SAclVO.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. SBucketVO 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.getId(), 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 \nAllowOverride FileInfo AuthConfig Limit... 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 SBucketVO 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 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.append(""); xml.append("").append(bucketName).append(""); xml.append("").append((null == keyMarker ? "" : keyMarker)) .append(""); xml.append("") .append((null == uploadIdMarker ? "" : uploadIdMarker)) .append(""); // [C] Construct the contents of the 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(""); partsList.append("").append(nextKey).append(""); partsList.append("").append(nextUploadId) .append(""); partsList.append(""); partsList.append("").append(onePart.getAccessKey()) .append(""); partsList.append(""); partsList.append(""); partsList.append(""); partsList.append("").append(onePart.getAccessKey()) .append(""); partsList.append(""); partsList.append(""); partsList.append("STANDARD"); partsList .append("") .append(DatatypeConverter.printDateTime(onePart .getLastModified())).append(""); partsList.append(""); } // [D] Construct the contents of the 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(""); partsList.append(""); if (prefix != null && prefix.length() > 0) partsList.append(prefix + delimiter + subName); else partsList.append(subName); partsList.append(""); partsList.append(""); } } } // [D] Finish off the response xml.append("").append((null == nextKey ? "" : nextKey)) .append(""); xml.append("") .append((0 == nextUploadId ? "" : nextUploadId)) .append(""); xml.append("").append(maxUploads).append(""); xml.append("").append(isTruncated) .append(""); xml.append(partsList.toString()); xml.append(""); 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; } }