Package org.gaul.s3proxy

Source Code of org.gaul.s3proxy.S3ProxyHandler

/*
* Copyright 2014 Andrew Gaul <andrew@gaul.org>
*
* 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 org.gaul.s3proxy;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.collect.TreeMultimap;
import com.google.common.hash.HashCode;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import com.google.common.net.HttpHeaders;

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.jclouds.blobstore.BlobStore;
import org.jclouds.blobstore.ContainerNotFoundException;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.blobstore.domain.BlobBuilder;
import org.jclouds.blobstore.domain.BlobMetadata;
import org.jclouds.blobstore.domain.PageSet;
import org.jclouds.blobstore.domain.StorageMetadata;
import org.jclouds.blobstore.domain.StorageType;
import org.jclouds.blobstore.options.CreateContainerOptions;
import org.jclouds.blobstore.options.GetOptions;
import org.jclouds.blobstore.options.ListContainerOptions;
import org.jclouds.blobstore.options.PutOptions;
import org.jclouds.domain.Location;
import org.jclouds.http.HttpResponseException;
import org.jclouds.io.ContentMetadata;
import org.jclouds.rest.AuthorizationException;
import org.jclouds.util.Strings2;
import org.jclouds.util.Throwables2;
import org.slf4j.Logger;
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 =
            "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
    private static final String AWS_XMLNS =
            "xmlns=\"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
    private static final String FAKE_OWNER_ID =
            "75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a";
    private static final String FAKE_OWNER_DISPLAY_NAME =
            "CustomersName@amazon.com";
    private static final String FAKE_REQUEST_ID = "4442587FB7D0A2F9";
    private static final Pattern VALID_BUCKET_PATTERN =
            Pattern.compile("[a-zA-Z0-9._-]+");
    private static final Pattern CREATE_BUCKET_LOCATION_PATTERN =
            Pattern.compile("<LocationConstraint>(.*?)</LocationConstraint>");
    private static final Pattern MULTI_DELETE_KEY_PATTERN =
            Pattern.compile("<Key>(.*?)</Key>");

    private final BlobStore blobStore;
    private final String identity;
    private final String credential;
    private final boolean forceMultiPartUpload;
    private final Optional<String> virtualHost;

    S3ProxyHandler(BlobStore blobStore, String identity, String credential,
            boolean forceMultiPartUpload, Optional<String> virtualHost) {
        this.blobStore = Preconditions.checkNotNull(blobStore);
        this.identity = identity;
        this.credential = credential;
        this.forceMultiPartUpload = forceMultiPartUpload;
        this.virtualHost = Preconditions.checkNotNull(virtualHost);
    }

    @Override
    public void handle(String target, Request baseRequest,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException {
        String method = request.getMethod();
        String uri = request.getRequestURI();
        logger.debug("request: {}", request);
        String hostHeader = request.getHeader(HttpHeaders.HOST);
        if (hostHeader != null && virtualHost.isPresent()) {
            String virtualHostSuffix = "." + virtualHost.get();
            if (hostHeader.endsWith(virtualHostSuffix)) {
                String bucket = hostHeader.substring(0,
                        hostHeader.length() - virtualHostSuffix.length());
                uri = "/" + bucket + uri;
            }
        }

        boolean hasDateHeader = false;
        boolean hasXAmzDateHeader = false;
        for (String headerName : Collections.list(request.getHeaderNames())) {
            for (String headerValue : Collections.list(request.getHeaders(
                    headerName))) {
                logger.trace("header: {}: {}", headerName,
                        Strings.nullToEmpty(headerValue));
            }
            if (headerName.equalsIgnoreCase(HttpHeaders.DATE)) {
                hasDateHeader = true;
            } else if (headerName.equalsIgnoreCase("x-amz-date")) {
                hasXAmzDateHeader = true;
            }
        }

        if (identity != null && !hasDateHeader && !hasXAmzDateHeader &&
                request.getParameter("Expires") == null) {
            sendSimpleErrorResponse(response, S3ErrorCode.ACCESS_DENIED);
            baseRequest.setHandled(true);
            return;
        }

        // TODO: apply sanity checks to X-Amz-Date
        if (hasDateHeader) {
            long date;
            try {
                date = request.getDateHeader(HttpHeaders.DATE);
            } catch (IllegalArgumentException iae) {
                sendSimpleErrorResponse(response,
                        S3ErrorCode.ACCESS_DENIED);
                baseRequest.setHandled(true);
                return;
            }
            if (date < 0) {
                sendSimpleErrorResponse(response,
                        S3ErrorCode.ACCESS_DENIED);
                baseRequest.setHandled(true);
                return;
            }
            long now = System.currentTimeMillis();
            if (now + TimeUnit.DAYS.toMillis(1) < date ||
                    now - TimeUnit.DAYS.toMillis(1) > date) {
                sendSimpleErrorResponse(response,
                        S3ErrorCode.REQUEST_TIME_TOO_SKEWED);
                baseRequest.setHandled(true);
                return;
            }
        }

        if (identity != null) {
            String expectedSignature = createAuthorizationSignature(request,
                    uri, identity, credential);
            String headerAuthorization = request.getHeader(
                    HttpHeaders.AUTHORIZATION);
            String headerIdentity = null;
            String headerSignature = null;
            if (headerAuthorization != null &&
                    headerAuthorization.startsWith("AWS ")) {
                String[] values =
                        headerAuthorization.substring(4).split(":", 2);
                if (values.length != 2) {
                    sendSimpleErrorResponse(response,
                            S3ErrorCode.INVALID_ARGUMENT);
                    baseRequest.setHandled(true);
                    return;
                }
                headerIdentity = values[0];
                headerSignature = values[1];
            }
            String parameterIdentity = request.getParameter("AWSAccessKeyId");
            String parameterSignature = request.getParameter("Signature");

            if (headerIdentity != null && headerSignature != null) {
                if (!identity.equals(headerIdentity) ||
                        !expectedSignature.equals(headerSignature)) {
                    sendSimpleErrorResponse(response,
                            S3ErrorCode.SIGNATURE_DOES_NOT_MATCH);
                    baseRequest.setHandled(true);
                    return;
                }
            } else if (parameterIdentity != null &&
                    parameterSignature != null) {
                if (!identity.equals(parameterIdentity) ||
                        !expectedSignature.equals(parameterSignature)) {
                    sendSimpleErrorResponse(response,
                            S3ErrorCode.SIGNATURE_DOES_NOT_MATCH);
                    baseRequest.setHandled(true);
                    return;
                }

                String expiresString = request.getParameter("Expires");
                if (expiresString != null) {
                    long expires = Long.parseLong(expiresString);
                    long nowSeconds = System.currentTimeMillis() / 1000;
                    if (nowSeconds > expires) {
                        sendSimpleErrorResponse(response,
                                S3ErrorCode.ACCESS_DENIED);
                        baseRequest.setHandled(true);
                        return;
                    }
                }
            } else {
                sendSimpleErrorResponse(response, S3ErrorCode.ACCESS_DENIED);
                baseRequest.setHandled(true);
                return;
            }
        }

        String[] path = uri.split("/", 3);
        switch (method) {
        case "DELETE":
            if (path.length <= 2 || path[2].isEmpty()) {
                handleContainerDelete(response, path[1]);
                baseRequest.setHandled(true);
                return;
            } else {
                handleBlobRemove(response, path[1], path[2]);
                baseRequest.setHandled(true);
                return;
            }
        case "GET":
            if (uri.equals("/")) {
                handleContainerList(response);
                baseRequest.setHandled(true);
                return;
            } else if (path.length <= 2 || path[2].isEmpty()) {
                if ("".equals(request.getParameter("acl"))) {
                    handleContainerOrBlobAcl(response, path[1]);
                    baseRequest.setHandled(true);
                    return;
                }
                handleBlobList(request, response, path[1]);
                baseRequest.setHandled(true);
                return;
            } else {
                if ("".equals(request.getParameter("acl"))) {
                    handleContainerOrBlobAcl(response, path[1], path[2]);
                    baseRequest.setHandled(true);
                    return;
                }
                handleGetBlob(request, response, path[1], path[2]);
                baseRequest.setHandled(true);
                return;
            }
        case "HEAD":
            if (path.length <= 2 || path[2].isEmpty()) {
                handleContainerExists(response, path[1]);
                baseRequest.setHandled(true);
                return;
            } else {
                handleBlobMetadata(response, path[1], path[2]);
                baseRequest.setHandled(true);
                return;
            }
        case "POST":
            if ("".equals(request.getParameter("delete"))) {
                handleMultiBlobRemove(request, response, path[1]);
                baseRequest.setHandled(true);
                return;
            }
            break;
        case "PUT":
            if (path.length <= 2 || path[2].isEmpty()) {
                if ("".equals(request.getParameter("acl"))) {
                    response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
                    baseRequest.setHandled(true);
                    return;
                }
                handleContainerCreate(request, response, path[1]);
                baseRequest.setHandled(true);
                return;
            } else if (request.getHeader("x-amz-copy-source") != null) {
                handleCopyBlob(request, response, path[1], path[2]);
                baseRequest.setHandled(true);
                return;
            } else {
                if ("".equals(request.getParameter("acl"))) {
                    response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
                    baseRequest.setHandled(true);
                    return;
                }
                handlePutBlob(request, response, path[1], path[2]);
                baseRequest.setHandled(true);
                return;
            }
        default:
            logger.error("Unknown method {} with URI {}",
                    method, request.getRequestURI());
            response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
            baseRequest.setHandled(true);
            return;
        }
    }

    private void handleContainerOrBlobAcl(HttpServletResponse response,
            String... containerName) throws IOException {
        try (Writer writer = response.getWriter()) {
            writer.write("<AccessControlPolicy>\r\n" +
                    "  <Owner>\r\n" +
                    "    <ID>" + FAKE_OWNER_ID + "</ID>\r\n" +
                    "    <DisplayName>" + FAKE_OWNER_DISPLAY_NAME +
                    "</DisplayName>\r\n" +
                    "  </Owner>\r\n" +
                    "  <AccessControlList>\r\n" +
                    "    <Grant>\r\n" +
                    "      <Grantee xmlns:xsi=" +
                    "\"http://www.w3.org/2001/XMLSchema-instance\"\r\n" +
                    "            xsi:type=\"CanonicalUser\">\r\n" +
                    "        <ID>" + FAKE_OWNER_ID + "</ID>\r\n" +
                    "        <DisplayName>" + FAKE_OWNER_DISPLAY_NAME +
                    "</DisplayName>\r\n" +
                    "      </Grantee>\r\n" +
                    "      <Permission>FULL_CONTROL</Permission>\r\n" +
                    "    </Grant>\r\n" +
                    "  </AccessControlList>\r\n" +
                    "</AccessControlPolicy>");
            writer.flush();
        }
    }

    private void handleContainerList(HttpServletResponse response)
            throws IOException {
        try (Writer writer = response.getWriter()) {
            writer.write(XML_PROLOG +
                    "<ListAllMyBucketsResult " + AWS_XMLNS + ">\r\n" +
                    "  <Owner>\r\n" +
                    "    <ID>" + FAKE_OWNER_ID + "</ID>\r\n" +
                    "    <DisplayName>" + FAKE_OWNER_DISPLAY_NAME +
                    "</DisplayName>\r\n" +
                    "  </Owner>\r\n" +
                    "  <Buckets>\r\n");

            for (StorageMetadata metadata : blobStore.list()) {
                writer.write("    <Bucket>\r\n" +
                        "      <Name>");
                writer.write(metadata.getName());
                writer.write("</Name>\r\n");
                Date creationDate = metadata.getCreationDate();
                if (creationDate != null) {
                    writer.write("      <CreationDate>");
                    writer.write(blobStore.getContext().utils().date()
                            .iso8601DateFormat(creationDate).trim());
                    writer.write("</CreationDate>\r\n");
                }
                writer.write("    </Bucket>\r\n");
            }

            writer.write("  </Buckets>\r\n" +
                    "</ListAllMyBucketsResult>");
            writer.flush();
        }
    }

    private void handleContainerExists(HttpServletResponse response,
            String containerName) throws IOException {
        if (!blobStore.containerExists(containerName)) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_BUCKET);
            return;
        }
    }

    private void handleContainerCreate(HttpServletRequest request,
            HttpServletResponse response, String containerName)
            throws IOException {
        if (containerName.isEmpty()) {
            sendSimpleErrorResponse(response, S3ErrorCode.METHOD_NOT_ALLOWED);
            return;
        }
        if (containerName.length() < 3 || containerName.length() > 255 ||
                !VALID_BUCKET_PATTERN.matcher(containerName).matches()) {
            sendSimpleErrorResponse(response, S3ErrorCode.INVALID_BUCKET_NAME);
            return;
        }

        Location location = null;
        // TODO: more robust XML parsing
        Matcher matcher = CREATE_BUCKET_LOCATION_PATTERN.matcher(
                Strings2.toStringAndClose(request.getInputStream()));
        if (matcher.find()) {
            String locationString = matcher.group(1);
            for (Location loc : blobStore.listAssignableLocations()) {
                if (loc.getId().equalsIgnoreCase(locationString)) {
                    location = loc;
                    break;
                }
            }
            if (location == null) {
                sendSimpleErrorResponse(response,
                        S3ErrorCode.INVALID_LOCATION_CONSTRAINT);
                return;
            }
        }
        logger.debug("Creating bucket with location: {}", location);

        CreateContainerOptions options = new CreateContainerOptions();
        String acl = request.getHeader("x-amz-acl");
        if ("public-read".equals(acl)) {
            options.publicRead();
        }

        try {
            if (blobStore.createContainerInLocation(location, containerName,
                    options)) {
                return;
            }
            sendSimpleErrorResponse(response,
                    S3ErrorCode.BUCKET_ALREADY_OWNED_BY_YOU,
                    Optional.of("  <BucketName>" + containerName +
                            "</BucketName>\r\n"));
        } catch (AuthorizationException ae) {
            sendSimpleErrorResponse(response,
                    S3ErrorCode.BUCKET_ALREADY_EXISTS);
            return;
        }
    }

    private void handleContainerDelete(HttpServletResponse response,
            String containerName) throws IOException {
        if (!blobStore.containerExists(containerName)) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_BUCKET);
            return;
        }
        if (!blobStore.deleteContainerIfEmpty(containerName)) {
            sendSimpleErrorResponse(response, S3ErrorCode.BUCKET_NOT_EMPTY);
            return;
        }
        response.setStatus(HttpServletResponse.SC_NO_CONTENT);
    }

    private void handleBlobList(HttpServletRequest request,
            HttpServletResponse response, String containerName)
            throws IOException {
        ListContainerOptions options = new ListContainerOptions();
        String delimiter = request.getParameter("delimiter");
        if (!(delimiter != null && delimiter.equals("/"))) {
            options = options.recursive();
        }
        String prefix = request.getParameter("prefix");
        if (prefix != null) {
            options = options.inDirectory(prefix);
        }
        String marker = request.getParameter("marker");
        if (marker != null) {
            options = options.afterMarker(request.getParameter("marker"));
        }
        int maxKeys = 1000;
        String maxKeysString = request.getParameter("max-keys");
        if (maxKeysString != null) {
            try {
                maxKeys = Integer.parseInt(maxKeysString);
            } catch (NumberFormatException nfe) {
                sendSimpleErrorResponse(response, S3ErrorCode.INVALID_ARGUMENT);
                return;
            }
        }
        options = options.maxResults(maxKeys);

        PageSet<? extends StorageMetadata> set;
        try {
            set = blobStore.list(containerName, options);
        } catch (ContainerNotFoundException cnfe) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_BUCKET);
            return;
        }

        try (Writer writer = response.getWriter()) {
            response.setStatus(HttpServletResponse.SC_OK);
            writer.write(XML_PROLOG +
                    "<ListBucketResult " + AWS_XMLNS + ">\r\n" +
                    "  <Name>");
            writer.write(containerName);
            writer.write("</Name>\r\n");
            if (prefix == null) {
                writer.write("  <Prefix/>\r\n");
            } else {
                writer.write("  <Prefix>");
                writer.write(prefix);
                writer.write("</Prefix>\r\n");
            }
            writer.write("  <MaxKeys>");
            writer.write(String.valueOf(maxKeys));
            writer.write("</MaxKeys>\r\n");
            if (marker == null) {
                writer.write("  <Marker/>\r\n");
            } else {
                writer.write("  <Marker>");
                writer.write(marker);
                writer.write("</Marker>\r\n");
            }
            if (delimiter != null) {
                writer.write("  <Delimiter>");
                writer.write(delimiter);
                writer.write("</Delimiter>\r\n");
            }
            String nextMarker = set.getNextMarker();
            if (nextMarker != null) {
                writer.write("  <IsTruncated>true</IsTruncated>\r\n" +
                    "  <NextMarker>");
                writer.write(nextMarker);
                writer.write("</NextMarker>\r\n");
            } else {
                writer.write("  <IsTruncated>false</IsTruncated>\r\n");
            }

            Set<String> commonPrefixes = new TreeSet<>();
            for (StorageMetadata metadata : set) {
                if (metadata.getType() != StorageType.BLOB) {
                    commonPrefixes.add(metadata.getName());
                    continue;
                }
                writer.write("  <Contents>\r\n" +
                    "    <Key>");
                writer.write(metadata.getName());
                writer.write("</Key>\r\n");
                Date lastModified = metadata.getLastModified();
                if (lastModified != null) {
                    writer.write("    <LastModified>");
                    writer.write(blobStore.getContext().utils().date()
                            .iso8601DateFormat(lastModified));
                    writer.write("</LastModified>\r\n");
                }
                String eTag = metadata.getETag();
                if (eTag != null) {
                    String id = blobStore.getContext().unwrap()
                            .getProviderMetadata().getId();
                    writer.write("    <ETag>&quot;");
                    if (id.equals("google-cloud-storage")) {
                        eTag = BaseEncoding.base16().lowerCase().encode(
                                BaseEncoding.base64().decode(eTag));
                    }
                    writer.write(eTag);
                    writer.write("&quot;</ETag>\r\n");
                }
                writer.write(
                    // TODO: StorageMetadata does not contain size
                    "    <Size>0</Size>\r\n" +
                    "    <StorageClass>STANDARD</StorageClass>\r\n" +
                    "    <Owner>\r\n" +
                    "      <ID>" + FAKE_OWNER_ID + "</ID>\r\n" +
                    "      <DisplayName>" + FAKE_OWNER_DISPLAY_NAME +
                    "</DisplayName>\r\n" +
                    "    </Owner>\r\n" +
                    "  </Contents>\r\n");
            }

            for (String commonPrefix : commonPrefixes) {
                writer.write("  <CommonPrefixes>\r\n" +
                        "    <Prefix>");
                writer.write(commonPrefix);
                if (delimiter != null) {
                    writer.write(delimiter);
                }
                writer.write("</Prefix>\r\n" +
                        "  </CommonPrefixes>\r\n");
            }

            writer.write("</ListBucketResult>");
            writer.flush();
        }
    }

    private void handleBlobRemove(HttpServletResponse response,
            String containerName, String blobName) throws IOException {
        try {
            blobStore.removeBlob(containerName, blobName);
            response.sendError(HttpServletResponse.SC_NO_CONTENT);
        } catch (ContainerNotFoundException cnfe) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_BUCKET);
            return;
        }
    }

    private void handleMultiBlobRemove(HttpServletRequest request,
            HttpServletResponse response, String containerName)
            throws IOException {
        try (Writer writer = response.getWriter()) {
            writer.write(XML_PROLOG);
            writer.write("<DeleteResult " + AWS_XMLNS + ">\r\n");
            // TODO: more robust XML parsing
            Matcher matcher = MULTI_DELETE_KEY_PATTERN.matcher(
                    Strings2.toStringAndClose(request.getInputStream()));
            while (matcher.find()) {
                String blobName = matcher.group(1);
                blobStore.removeBlob(containerName, blobName);

                writer.write("<Deleted><Key>");
                writer.write(blobName);
                writer.write("</Key></Deleted>\r\n");
            }
            // TODO: emit error stanza
            writer.write("</DeleteResult>");
        }
    }

    private void handleBlobMetadata(HttpServletResponse response,
            String containerName, String blobName) throws IOException {
        BlobMetadata metadata;
        try {
            metadata = blobStore.blobMetadata(containerName, blobName);
        } catch (ContainerNotFoundException cnfe) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_BUCKET);
            return;
        }
        if (metadata == null) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_KEY);
            return;
        }

        response.setStatus(HttpServletResponse.SC_OK);
        addMetadataToResponse(response, metadata);
    }

    private void handleGetBlob(HttpServletRequest request,
            HttpServletResponse response, String containerName,
            String blobName) throws IOException {
        int status = HttpServletResponse.SC_OK;
        GetOptions options = new GetOptions();
        String range = request.getHeader(HttpHeaders.RANGE);
        if (range != null && range.startsWith("bytes=") &&
                // ignore multiple ranges
                range.indexOf(',') == -1) {
            range = range.substring("bytes=".length());
            String[] ranges = range.split("-", 2);
            if (ranges[0].isEmpty()) {
                options = options.tail(Long.parseLong(ranges[1]));
            } else if (ranges[1].isEmpty()) {
                options = options.startAt(Long.parseLong(ranges[0]));
            } else {
                options = options.range(Long.parseLong(ranges[0]),
                        Long.parseLong(ranges[1]));
            }
            status = HttpServletResponse.SC_PARTIAL_CONTENT;
        }

        Blob blob;
        try {
            blob = blobStore.getBlob(containerName, blobName, options);
        } catch (ContainerNotFoundException cnfe) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_BUCKET);
            return;
        }
        if (blob == null) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_KEY);
            return;
        }

        response.setStatus(status);
        addMetadataToResponse(response, blob.getMetadata());
        try (InputStream is = blob.getPayload().openStream();
             OutputStream os = response.getOutputStream()) {
            ByteStreams.copy(is, os);
            os.flush();
        }
    }

    private void handleCopyBlob(HttpServletRequest request,
            HttpServletResponse response, String destContainerName,
            String destBlobName) throws IOException {
        String copySourceHeader = request.getHeader("x-amz-copy-source");
        if (copySourceHeader.startsWith("/")) {
            // Some clients like boto do not include the leading slash
            copySourceHeader = copySourceHeader.substring(1);
        }
        String[] path = copySourceHeader.split("/", 2);
        String sourceContainerName = path[0];
        String sourceBlobName = path[1];
        boolean replaceMetadata = "REPLACE".equals(request.getHeader(
                "x-amz-metadata-directive"));

        ImmutableMap.Builder<String, String> userMetadataBuilder =
                ImmutableMap.builder();
        for (String headerName : Collections.list(request.getHeaderNames())) {
            if (!headerName.startsWith(USER_METADATA_PREFIX)) {
                continue;
            }
            userMetadataBuilder.put(
                    headerName.substring(USER_METADATA_PREFIX.length()),
                    Strings.nullToEmpty(request.getHeader(headerName)));
        }
        Map<String, String> userMetadata = userMetadataBuilder.build();

        if (sourceContainerName.equals(destContainerName) &&
                sourceBlobName.equals(destBlobName) &&
                !replaceMetadata) {
            sendSimpleErrorResponse(response, S3ErrorCode.INVALID_REQUEST);
            return;
        }

        Blob blob = blobStore.getBlob(sourceContainerName, sourceBlobName);
        if (blob == null) {
            sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_KEY);
            return;
        }

        try (InputStream is = blob.getPayload().openStream()) {
            ContentMetadata metadata = blob.getMetadata().getContentMetadata();
            BlobBuilder.PayloadBlobBuilder builder = blobStore
                    .blobBuilder(destBlobName)
                    .userMetadata(replaceMetadata ? userMetadata :
                            blob.getMetadata().getUserMetadata())
                    .payload(is)
                    .contentDisposition(metadata.getContentDisposition())
                    .contentEncoding(metadata.getContentEncoding())
                    .contentLanguage(metadata.getContentLanguage())
                    .contentLength(metadata.getContentLength())
                    .contentType(metadata.getContentType());

            String eTag = blobStore.putBlob(destContainerName,
                    builder.build());
            Date lastModified = blob.getMetadata().getLastModified();
            try (Writer writer = response.getWriter()) {
                writer.write(XML_PROLOG);
                writer.write("<CopyObjectResult>\r\n");
                writer.write("  <LastModified>");
                writer.write(blobStore.getContext().utils().date()
                        .iso8601DateFormat(lastModified));
                writer.write("</LastModified>\r\n");
                writer.write("  <ETag>&quot;");
                writer.write(eTag);
                writer.write("&quot;</ETag>\r\n");
                writer.write("</CopyObjectResult>");
            }
        }
    }

    private void handlePutBlob(HttpServletRequest request,
            HttpServletResponse response, String containerName,
            String blobName) throws IOException {
        // Flag headers present since HttpServletResponse.getHeader returns
        // null for empty headers.
        boolean hasContentLength = false;
        boolean hasContentMD5 = false;
        ImmutableMap.Builder<String, String> userMetadata =
                ImmutableMap.builder();
        for (String headerName : Collections.list(request.getHeaderNames())) {
            if (headerName.equals(HttpHeaders.CONTENT_LENGTH)) {
                hasContentLength = true;
            } else if (headerName.equals(HttpHeaders.CONTENT_MD5)) {
                hasContentMD5 = true;
            } else if (headerName.toLowerCase().startsWith(
                    USER_METADATA_PREFIX)) {
                userMetadata.put(
                        headerName.substring(USER_METADATA_PREFIX.length()),
                        Strings.nullToEmpty(request.getHeader(headerName)));
            }
        }

        HashCode contentMD5 = null;
        if (hasContentMD5) {
            boolean validDigest = true;
            String contentMD5String = request.getHeader(
                    HttpHeaders.CONTENT_MD5);
            if (contentMD5String == null) {
                validDigest = false;
            } else {
                try {
                    contentMD5 = HashCode.fromBytes(
                            BaseEncoding.base64().decode(contentMD5String));
                } catch (IllegalArgumentException iae) {
                    validDigest = false;
                }
            }
            if (!validDigest) {
                sendSimpleErrorResponse(response, S3ErrorCode.INVALID_DIGEST);
                return;
            }
        }

        if (!hasContentLength) {
            sendSimpleErrorResponse(response,
                    S3ErrorCode.MISSING_CONTENT_LENGTH);
            return;
        }
        long contentLength = 0;
        boolean validContentLength = true;
        String contentLengthString = request.getHeader(
                HttpHeaders.CONTENT_LENGTH);
        if (contentLengthString == null) {
            validContentLength = false;
        } else {
            try {
                contentLength = Long.parseLong(contentLengthString);
            } catch (NumberFormatException nfe) {
                validContentLength = false;
            }
        }
        if (!validContentLength || contentLength < 0) {
            sendSimpleErrorResponse(response, S3ErrorCode.INVALID_ARGUMENT);
            return;
        }

        try (InputStream is = request.getInputStream()) {
            BlobBuilder.PayloadBlobBuilder builder = blobStore
                    .blobBuilder(blobName)
                    .userMetadata(userMetadata.build())
                    .payload(is)
                    .contentDisposition(request.getHeader(
                            HttpHeaders.CONTENT_DISPOSITION))
                    .contentEncoding(request.getHeader(
                            HttpHeaders.CONTENT_ENCODING))
                    .contentLanguage(request.getHeader(
                            HttpHeaders.CONTENT_LANGUAGE))
                    .contentLength(request.getContentLength());
            String contentType = request.getContentType();
            if (contentType != null) {
                builder.contentType(contentType);
            }
            long expires = request.getDateHeader(HttpHeaders.EXPIRES);
            if (expires != -1) {
                builder = builder.expires(new Date(expires));
            }
            if (contentMD5 != null) {
                builder = builder.contentMD5(contentMD5);
            }

            PutOptions options = new PutOptions()
                .multipart(forceMultiPartUpload);
            try {
                String eTag = blobStore.putBlob(containerName, builder.build(),
                        options);
                // S3 quotes ETag while Swift does not
                if (!eTag.startsWith("\"") && !eTag.endsWith("\"")) {
                    eTag = '"' + eTag + '"';
                }
                response.addHeader(HttpHeaders.ETAG, eTag);
            } catch (ContainerNotFoundException cnfe) {
                sendSimpleErrorResponse(response, S3ErrorCode.NO_SUCH_BUCKET);
                return;
            } catch (HttpResponseException hre) {
                int status = hre.getResponse().getStatusCode();
                switch (status) {
                case HttpServletResponse.SC_BAD_REQUEST:
                case 422// Swift returns 422 Unprocessable Entity
                    sendSimpleErrorResponse(response,
                            S3ErrorCode.BAD_DIGEST);
                    break;
                default:
                    // TODO: emit hre.getContent() ?
                    response.sendError(status);
                    break;
                }
                return;
            } catch (RuntimeException re) {
                if (Throwables2.getFirstThrowableOfType(re,
                        TimeoutException.class) != null) {
                    sendSimpleErrorResponse(response,
                            S3ErrorCode.REQUEST_TIMEOUT);
                    return;
                } else {
                    throw re;
                }
            }
        }
    }

    private static void addMetadataToResponse(HttpServletResponse response,
            BlobMetadata metadata) {
        ContentMetadata contentMetadata =
                metadata.getContentMetadata();
        response.addHeader(HttpHeaders.CONTENT_DISPOSITION,
                contentMetadata.getContentDisposition());
        response.addHeader(HttpHeaders.CONTENT_ENCODING,
                contentMetadata.getContentEncoding());
        response.addHeader(HttpHeaders.CONTENT_LANGUAGE,
                contentMetadata.getContentLanguage());
        response.addHeader(HttpHeaders.CONTENT_LENGTH,
                contentMetadata.getContentLength().toString());
        response.setContentType(contentMetadata.getContentType());
        HashCode contentMd5 = contentMetadata.getContentMD5AsHashCode();
        if (contentMd5 != null) {
            byte[] contentMd5Bytes = contentMd5.asBytes();
            response.addHeader(HttpHeaders.CONTENT_MD5,
                    BaseEncoding.base64().encode(contentMd5Bytes));
            response.addHeader(HttpHeaders.ETAG, "\"" +
                    BaseEncoding.base16().lowerCase().encode(contentMd5Bytes) +
                    "\"");
        }
        Date expires = contentMetadata.getExpires();
        if (expires != null) {
            response.addDateHeader(HttpHeaders.EXPIRES, expires.getTime());
        }
        response.addDateHeader(HttpHeaders.LAST_MODIFIED,
                metadata.getLastModified().getTime());
        for (Map.Entry<String, String> entry :
                metadata.getUserMetadata().entrySet()) {
            response.addHeader(USER_METADATA_PREFIX + entry.getKey(),
                    entry.getValue());
        }
    }

    private static void sendSimpleErrorResponse(HttpServletResponse response,
            S3ErrorCode code) throws IOException {
        sendSimpleErrorResponse(response, code, Optional.<String>absent());
    }

    private static void sendSimpleErrorResponse(HttpServletResponse response,
            S3ErrorCode code, Optional<String> extra) throws IOException {
        logger.debug("{} {}", code, extra);
        try (Writer writer = response.getWriter()) {
            response.setStatus(code.getHttpStatusCode());
            writer.write(XML_PROLOG +
                    "<Error>\r\n" +
                    "  <Code>");
            writer.write(code.getErrorCode());
            writer.write("</Code>\r\n" +
                    "  <Message>");
            writer.write(code.getMessage());
            writer.write("</Message>\r\n");
            if (extra.isPresent()) {
                writer.write(extra.get());
            }
            writer.write("  <RequestId>" + FAKE_REQUEST_ID +
                    "</RequestId>\r\n" +
                    "</Error>");
            writer.flush();
        }
    }

    /**
     * Create Amazon V2 signature.  Reference:
     * http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
     */
    private static String createAuthorizationSignature(
            HttpServletRequest request, String uri, String identity,
            String credential) {
        // sort Amazon headers
        SortedSetMultimap<String, String> canonicalizedHeaders =
                TreeMultimap.create();
        for (String headerName : Collections.list(request.getHeaderNames())) {
            Collection<String> headerValues = Collections.list(
                    request.getHeaders(headerName));
            headerName = headerName.toLowerCase();
            if (!headerName.startsWith("x-amz-")) {
                continue;
            }
            if (headerValues.isEmpty()) {
                canonicalizedHeaders.put(headerName, "");
            }
            for (String headerValue : headerValues) {
                canonicalizedHeaders.put(headerName,
                        Strings.nullToEmpty(headerValue));
            }
        }

        // build string to sign
        StringBuilder builder = new StringBuilder()
                .append(request.getMethod())
                .append('\n')
                .append(Strings.nullToEmpty(request.getHeader(
                        HttpHeaders.CONTENT_MD5)))
                .append('\n')
                .append(Strings.nullToEmpty(request.getHeader(
                        HttpHeaders.CONTENT_TYPE)))
                .append('\n');
        String expires = request.getParameter("Expires");
        if (expires != null) {
            builder.append(expires);
        } else if (!canonicalizedHeaders.containsKey("x-amz-date")) {
            builder.append(request.getHeader(HttpHeaders.DATE));
        }
        builder.append('\n');
        for (Map.Entry<String, String> entry : canonicalizedHeaders.entries()) {
            builder.append(entry.getKey()).append(':')
                    .append(entry.getValue()).append('\n');
        }
        builder.append(uri);
        if ("".equals(request.getParameter("acl"))) {
            builder.append("?acl");
        } else if ("".equals(request.getParameter("delete"))) {
            builder.append("?delete");
        }
        String stringToSign = builder.toString();
        logger.trace("stringToSign: {}", stringToSign);

        // sign string
        Mac mac;
        try {
            mac = Mac.getInstance("HmacSHA1");
            mac.init(new SecretKeySpec(credential.getBytes(
                    StandardCharsets.UTF_8), "HmacSHA1"));
        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
            throw Throwables.propagate(e);
        }
        return BaseEncoding.base64().encode(mac.doFinal(
                stringToSign.getBytes(StandardCharsets.UTF_8)));
    }
}
TOP

Related Classes of org.gaul.s3proxy.S3ProxyHandler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.