diff --git a/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java b/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java index 202346d..4fd354e 100644 --- a/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java +++ b/src/main/java/org/gaul/s3proxy/S3ProxyHandler.java @@ -40,6 +40,9 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; import com.google.common.base.Optional; import com.google.common.base.Preconditions; @@ -82,11 +85,8 @@ import org.slf4j.LoggerFactory; final class S3ProxyHandler extends AbstractHandler { private static final Logger logger = LoggerFactory.getLogger( S3ProxyHandler.class); - // Note that this excludes a trailing \r\n which the AWS SDK rejects. - private static final String XML_PROLOG = - ""; private static final String AWS_XMLNS = - "xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\""; + "http://s3.amazonaws.com/doc/2006-03-01/"; // TODO: support configurable metadata prefix private static final String USER_METADATA_PREFIX = "x-amz-meta-"; // TODO: fake owner @@ -112,6 +112,8 @@ final class S3ProxyHandler extends AbstractHandler { private final String credential; private final boolean forceMultiPartUpload; private final Optional virtualHost; + private final XMLOutputFactory xmlOutputFactory = + XMLOutputFactory.newInstance(); S3ProxyHandler(BlobStore blobStore, String identity, String credential, boolean forceMultiPartUpload, Optional virtualHost) { @@ -120,6 +122,8 @@ final class S3ProxyHandler extends AbstractHandler { this.credential = credential; this.forceMultiPartUpload = forceMultiPartUpload; this.virtualHost = Preconditions.checkNotNull(virtualHost); + xmlOutputFactory.setProperty("javax.xml.stream.isRepairingNamespaces", + Boolean.FALSE); } @Override @@ -358,70 +362,116 @@ final class S3ProxyHandler extends AbstractHandler { private void handleContainerOrBlobAcl(HttpServletResponse response, String... containerName) throws IOException { try (Writer writer = response.getWriter()) { - writer.write(XML_PROLOG + - "\r\n" + - " \r\n" + - " " + FAKE_OWNER_ID + "\r\n" + - " " + FAKE_OWNER_DISPLAY_NAME + - "\r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - " \r\n" + - " " + FAKE_OWNER_ID + "\r\n" + - " " + FAKE_OWNER_DISPLAY_NAME + - "\r\n" + - " \r\n" + - " FULL_CONTROL\r\n" + - " \r\n" + - " \r\n" + - ""); - writer.flush(); + XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter( + writer); + xml.writeStartDocument(); + xml.writeStartElement("AccessControlPolicy"); + xml.writeDefaultNamespace(AWS_XMLNS); + + xml.writeStartElement("Owner"); + + xml.writeStartElement("ID"); + xml.writeCharacters(FAKE_OWNER_ID); + xml.writeEndElement(); + + xml.writeStartElement("DisplayName"); + xml.writeCharacters(FAKE_OWNER_DISPLAY_NAME); + xml.writeEndElement(); + + xml.writeEndElement(); + + xml.writeStartElement("AccessControlList"); + xml.writeStartElement("Grant"); + + xml.writeStartElement("Grantee"); + xml.writeNamespace("xsi", + "http://www.w3.org/2001/XMLSchema-instance"); + xml.writeAttribute("xsi:type", "CanonicalUser"); + + xml.writeStartElement("ID"); + xml.writeCharacters(FAKE_OWNER_ID); + xml.writeEndElement(); + + xml.writeStartElement("DisplayName"); + xml.writeCharacters(FAKE_OWNER_DISPLAY_NAME); + xml.writeEndElement(); + + xml.writeEndElement(); + + xml.writeStartElement("Permission"); + xml.writeCharacters("FULL_CONTROL"); + xml.writeEndElement(); + + xml.writeEndElement(); + xml.writeEndElement(); + + xml.writeEndElement(); + xml.flush(); + } catch (XMLStreamException xse) { + throw new IOException(xse); } } private void handleContainerList(HttpServletResponse response) throws IOException { try (Writer writer = response.getWriter()) { - writer.write(XML_PROLOG + - "\r\n" + - " \r\n" + - " " + FAKE_OWNER_ID + "\r\n" + - " " + FAKE_OWNER_DISPLAY_NAME + - "\r\n" + - " \r\n" + - " \r\n"); + XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter( + writer); + xml.writeStartDocument(); + xml.writeStartElement("ListAllMyBucketsResult"); + xml.writeDefaultNamespace(AWS_XMLNS); + xml.writeStartElement("Owner"); + + xml.writeStartElement("ID"); + xml.writeCharacters(FAKE_OWNER_ID); + xml.writeEndElement(); + + xml.writeStartElement("DisplayName"); + xml.writeCharacters(FAKE_OWNER_DISPLAY_NAME); + xml.writeEndElement(); + + xml.writeEndElement(); + + xml.writeStartElement("Buckets"); for (StorageMetadata metadata : blobStore.list()) { - writer.write(" \r\n" + - " "); - writer.write(metadata.getName()); - writer.write("\r\n"); + xml.writeStartElement("Bucket"); + + xml.writeStartElement("Name"); + xml.writeCharacters(metadata.getName()); + xml.writeEndElement(); + Date creationDate = metadata.getCreationDate(); if (creationDate != null) { - writer.write(" "); - writer.write(blobStore.getContext().utils().date() + xml.writeStartElement("CreationDate"); + xml.writeCharacters(blobStore.getContext().utils().date() .iso8601DateFormat(creationDate).trim()); - writer.write("\r\n"); + xml.writeEndElement(); } - writer.write(" \r\n"); + xml.writeEndElement(); } + xml.writeEndElement(); - writer.write(" \r\n" + - ""); - writer.flush(); + xml.writeEndElement(); + xml.flush(); + } catch (XMLStreamException xse) { + throw new IOException(xse); } } private void handleContainerLocation(HttpServletResponse response, String containerName) throws IOException { try (Writer writer = response.getWriter()) { + XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter( + writer); + xml.writeStartDocument(); // TODO: using us-standard semantics but could emit actual location - writer.write(XML_PROLOG + - ""); - writer.flush(); + xml.writeStartElement("LocationConstraint"); + xml.writeDefaultNamespace(AWS_XMLNS); + xml.writeEndElement(); + xml.flush(); + } catch (XMLStreamException xse) { + throw new IOException(xse); } } @@ -494,9 +544,8 @@ final class S3ProxyHandler extends AbstractHandler { return; } sendSimpleErrorResponse(response, - S3ErrorCode.BUCKET_ALREADY_OWNED_BY_YOU, - Optional.of(" " + containerName + - "\r\n")); + S3ErrorCode.BUCKET_ALREADY_OWNED_BY_YOU, "BucketName", + containerName); } catch (AuthorizationException ae) { sendSimpleErrorResponse(response, S3ErrorCode.BUCKET_ALREADY_EXISTS); @@ -555,41 +604,55 @@ final class S3ProxyHandler extends AbstractHandler { try (Writer writer = response.getWriter()) { response.setStatus(HttpServletResponse.SC_OK); - writer.write(XML_PROLOG + - "\r\n" + - " "); - writer.write(containerName); - writer.write("\r\n"); + XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter( + writer); + xml.writeStartDocument(); + xml.writeStartElement("ListBucketResult"); + xml.writeDefaultNamespace(AWS_XMLNS); + + xml.writeStartElement("Name"); + xml.writeCharacters(containerName); + xml.writeEndElement(); + if (prefix == null) { - writer.write(" \r\n"); + xml.writeEmptyElement("Prefix"); } else { - writer.write(" "); - writer.write(prefix); - writer.write("\r\n"); + xml.writeStartElement("Prefix"); + xml.writeCharacters(prefix); + xml.writeEndElement(); } - writer.write(" "); - writer.write(String.valueOf(maxKeys)); - writer.write("\r\n"); + + xml.writeStartElement("MaxKeys"); + xml.writeCharacters(String.valueOf(maxKeys)); + xml.writeEndElement(); + if (marker == null) { - writer.write(" \r\n"); + xml.writeEmptyElement("Marker"); } else { - writer.write(" "); - writer.write(marker); - writer.write("\r\n"); + xml.writeStartElement("Marker"); + xml.writeCharacters(marker); + xml.writeEndElement(); } + if (delimiter != null) { - writer.write(" "); - writer.write(delimiter); - writer.write("\r\n"); + xml.writeStartElement("Delimiter"); + xml.writeCharacters(delimiter); + xml.writeEndElement(); } + String nextMarker = set.getNextMarker(); if (nextMarker != null) { - writer.write(" true\r\n" + - " "); - writer.write(nextMarker); - writer.write("\r\n"); + xml.writeStartElement("IsTruncated"); + xml.writeCharacters("true"); + xml.writeEndElement(); + + xml.writeStartElement("NextMarker"); + xml.writeCharacters(nextMarker); + xml.writeEndElement(); } else { - writer.write(" false\r\n"); + xml.writeStartElement("IsTruncated"); + xml.writeCharacters("false"); + xml.writeEndElement(); } Set commonPrefixes = new TreeSet<>(); @@ -598,54 +661,74 @@ final class S3ProxyHandler extends AbstractHandler { commonPrefixes.add(metadata.getName()); continue; } - writer.write(" \r\n" + - " "); - writer.write(metadata.getName()); - writer.write("\r\n"); + xml.writeStartElement("Contents"); + + xml.writeStartElement("Key"); + xml.writeCharacters(metadata.getName()); + xml.writeEndElement(); + Date lastModified = metadata.getLastModified(); if (lastModified != null) { - writer.write(" "); - writer.write(blobStore.getContext().utils().date() + xml.writeStartElement("LastModified"); + xml.writeCharacters(blobStore.getContext().utils().date() .iso8601DateFormat(lastModified)); - writer.write("\r\n"); + xml.writeEndElement(); } + String eTag = metadata.getETag(); if (eTag != null) { String id = blobStore.getContext().unwrap() .getProviderMetadata().getId(); - writer.write(" ""); + xml.writeStartElement("ETag"); if (id.equals("google-cloud-storage")) { eTag = BaseEncoding.base16().lowerCase().encode( BaseEncoding.base64().decode(eTag)); } - writer.write(eTag); - writer.write(""\r\n"); + xml.writeCharacters("\"" + eTag + "\""); + xml.writeEndElement(); } - writer.write( - // TODO: StorageMetadata does not contain size - " 0\r\n" + - " STANDARD\r\n" + - " \r\n" + - " " + FAKE_OWNER_ID + "\r\n" + - " " + FAKE_OWNER_DISPLAY_NAME + - "\r\n" + - " \r\n" + - " \r\n"); + + // TODO: StorageMetadata does not contain size + xml.writeStartElement("Size"); + xml.writeCharacters("0"); + xml.writeEndElement(); + + xml.writeStartElement("StorageClass"); + xml.writeCharacters("STANDARD"); + xml.writeEndElement(); + + xml.writeStartElement("Owner"); + + xml.writeStartElement("ID"); + xml.writeCharacters(FAKE_OWNER_ID); + xml.writeEndElement(); + + xml.writeStartElement("DisplayName"); + xml.writeCharacters(FAKE_OWNER_DISPLAY_NAME); + xml.writeEndElement(); + + xml.writeEndElement(); + + xml.writeEndElement(); } for (String commonPrefix : commonPrefixes) { - writer.write(" \r\n" + - " "); - writer.write(commonPrefix); + xml.writeStartElement("CommonPrefixes"); + + xml.writeStartElement("Prefix"); + xml.writeCharacters(commonPrefix); if (delimiter != null) { - writer.write(delimiter); + xml.writeCharacters(delimiter); } - writer.write("\r\n" + - " \r\n"); + xml.writeEndElement(); + + xml.writeEndElement(); } - writer.write(""); - writer.flush(); + xml.writeEndElement(); + xml.flush(); + } catch (XMLStreamException xse) { + throw new IOException(xse); } } @@ -664,8 +747,11 @@ final class S3ProxyHandler extends AbstractHandler { HttpServletResponse response, String containerName) throws IOException { try (Writer writer = response.getWriter()) { - writer.write(XML_PROLOG); - writer.write("\r\n"); + XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter( + writer); + xml.writeStartDocument(); + xml.writeStartElement("DeleteResult"); + xml.writeDefaultNamespace(AWS_XMLNS); // TODO: more robust XML parsing Matcher matcher = MULTI_DELETE_KEY_PATTERN.matcher( Strings2.toStringAndClose(request.getInputStream())); @@ -673,13 +759,17 @@ final class S3ProxyHandler extends AbstractHandler { String blobName = matcher.group(1); blobStore.removeBlob(containerName, blobName); - writer.write(""); - writer.write(blobName); - writer.write("\r\n"); + xml.writeStartElement("Deleted"); + xml.writeStartElement("Key"); + xml.writeCharacters(blobName); + xml.writeEndElement(); + xml.writeEndElement(); } // TODO: emit error stanza - writer.write(""); - writer.flush(); + xml.writeEndElement(); + xml.flush(); + } catch (XMLStreamException xse) { + throw new IOException(xse); } } @@ -800,17 +890,25 @@ final class S3ProxyHandler extends AbstractHandler { builder.build()); Date lastModified = blob.getMetadata().getLastModified(); try (Writer writer = response.getWriter()) { - writer.write(XML_PROLOG + - "\r\n"); - writer.write(" "); - writer.write(blobStore.getContext().utils().date() + XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter( + writer); + xml.writeStartDocument(); + xml.writeStartElement("CopyObjectResult"); + xml.writeDefaultNamespace(AWS_XMLNS); + + xml.writeStartElement("LastModified"); + xml.writeCharacters(blobStore.getContext().utils().date() .iso8601DateFormat(lastModified)); - writer.write("\r\n"); - writer.write(" ""); - writer.write(eTag); - writer.write(""\r\n"); - writer.write(""); - writer.flush(); + xml.writeEndElement(); + + xml.writeStartElement("ETag"); + xml.writeCharacters("\"" + eTag + "\""); + xml.writeEndElement(); + + xml.writeEndElement(); + xml.flush(); + } catch (XMLStreamException xse) { + throw new IOException(xse); } } } @@ -970,31 +1068,47 @@ final class S3ProxyHandler extends AbstractHandler { } } - private static void sendSimpleErrorResponse(HttpServletResponse response, + private void sendSimpleErrorResponse(HttpServletResponse response, S3ErrorCode code) throws IOException { - sendSimpleErrorResponse(response, code, Optional.absent()); + sendSimpleErrorResponse(response, code, null, null); } - private static void sendSimpleErrorResponse(HttpServletResponse response, - S3ErrorCode code, Optional extra) throws IOException { - logger.debug("{} {}", code, extra); + private void sendSimpleErrorResponse(HttpServletResponse response, + S3ErrorCode code, String element, String characters) + throws IOException { + Preconditions.checkArgument(!(element == null ^ characters == null), + "Must specify neither or both element and characters"); + logger.debug("{} {} {}", code, element, characters); + try (Writer writer = response.getWriter()) { + XMLStreamWriter xml = xmlOutputFactory.createXMLStreamWriter( + writer); response.setStatus(code.getHttpStatusCode()); - writer.write(XML_PROLOG + - "\r\n" + - " "); - writer.write(code.getErrorCode()); - writer.write("\r\n" + - " "); - writer.write(code.getMessage()); - writer.write("\r\n"); - if (extra.isPresent()) { - writer.write(extra.get()); + xml.writeStartDocument(); + xml.writeStartElement("Error"); + + xml.writeStartElement("Code"); + xml.writeCharacters(code.getErrorCode()); + xml.writeEndElement(); + + xml.writeStartElement("Message"); + xml.writeCharacters(code.getMessage()); + xml.writeEndElement(); + + if (element != null) { + xml.writeStartElement(element); + xml.writeCharacters(characters); + xml.writeEndElement(); } - writer.write(" " + FAKE_REQUEST_ID + - "\r\n" + - ""); - writer.flush(); + + xml.writeStartElement("RequestId"); + xml.writeCharacters(FAKE_REQUEST_ID); + xml.writeEndElement(); + + xml.writeEndElement(); + xml.flush(); + } catch (XMLStreamException xse) { + throw new IOException(xse); } }