/*
 *   Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License").
 *   You may not use this file except in compliance with the License.
 *   A copy of the License is located at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   or in the "license" file accompanying this file. This file 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.amazonaws.neptune.auth;
import com.amazonaws.SignableRequest;
import com.amazonaws.auth.AWSCredentialsProvider;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.StringEntity;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.amazonaws.auth.internal.SignerConstants.AUTHORIZATION;
import static com.amazonaws.auth.internal.SignerConstants.HOST;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_DATE;
import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_SECURITY_TOKEN;
/**
 * Signer for HTTP requests made via Apache Commons {@link HttpUriRequest}s.
 * 
 * Note that there are certain limitations for the usage of this class. In particular:
 * 
 *     -  The implementation adds a "Host" header. This may lead to problems if the original request has a host header
 *          with a name in different capitalization (e.g. "host"), leading to duplicate host headers and the signing
 *          process to fail. Hence, when using the API you need to make sure that there is either no host header in your
 *          original request or the host header uses the exact string "Host" as the header name.
 
 *     -  When using GET, the underlying HTTP request needs to encode whitespaces in query parameters using '%20'
 *          rather than (what most APIs such as the Apache commons {@link org.apache.http.client.utils.URIBuilder} do)
 *          using '+'.
 
 * 
 */
public class NeptuneApacheHttpSigV4Signer extends NeptuneSigV4SignerBase {
    /**
     * Create a V4 Signer for Apache Commons HTTP requests.
     *
     * @param regionName             name of the region for which the request is signed
     * @param awsCredentialsProvider the provider offering access to the credentials used for signing the request
     * @throws NeptuneSigV4SignerException in case initialization fails
     */
    public NeptuneApacheHttpSigV4Signer(
            final String regionName, final AWSCredentialsProvider awsCredentialsProvider)
            throws NeptuneSigV4SignerException {
        super(regionName, awsCredentialsProvider);
    }
    @Override
    protected SignableRequest> toSignableRequest(final HttpUriRequest request)
            throws NeptuneSigV4SignerException {
        // make sure the request is not null and contains the minimal required set of information
        checkNotNull(request, "The request must not be null");
        checkNotNull(request.getURI(), "The request URI must not be null");
        checkNotNull(request.getMethod(), "The request method must not be null");
        // convert the headers to the internal API format
        final Header[] headers = request.getAllHeaders();
        final Map headersInternal = new HashMap<>();
        for (final Header header : headers) {
            // Skip adding the Host header as the signing process will add one.
            if (!header.getName().equalsIgnoreCase(HOST)) {
                headersInternal.put(header.getName(), header.getValue());
            }
        }
        // convert the parameters to the internal API format
        final String queryStr = request.getURI().getRawQuery();
        final Map> parametersInternal = extractParametersFromQueryString(queryStr);
        // carry over the entity (or an empty entity, if no entity is provided)
        final InputStream content;
        try {
            HttpEntity httpEntity = null;
            if (request instanceof HttpEntityEnclosingRequest) {
                httpEntity = ((HttpEntityEnclosingRequest) request).getEntity();
            }
            // fallback: if we either have an HttpEntityEnclosingRequest without entity or
            //           say a GET request (which does not carry an entity), set the content
            //           to be an empty StringEntity as per the SigV4 spec
            if (httpEntity == null) {
                httpEntity = new StringEntity("");
            }
            content = httpEntity.getContent();
        } catch (final UnsupportedEncodingException e) {
            throw new NeptuneSigV4SignerException("Encoding of the input string failed", e);
        } catch (final IOException e) {
            throw new NeptuneSigV4SignerException("IOException while accessing entity content", e);
        }
        final URI uri = request.getURI();
        // http://example.com:8182 is the endpoint in http://example.com:8182/test/path
        URI endpoint;
        // /test/path is the resource path in http://example.com:8182/test/path
        String resourcePath;
        if (uri.getHost() != null) {
            endpoint = URI.create(uri.getScheme() + "://" + uri.getAuthority());
            resourcePath = uri.getPath();
        } else if (request instanceof HttpRequestWrapper) {
            final String host = ((HttpRequestWrapper) request).getTarget().toURI();
            endpoint = URI.create(host);
            resourcePath = uri.getPath();
        } else {
            throw new NeptuneSigV4SignerException(
                    "Unable to extract host information from the request uri, required for SigV4 signing: " + uri);
        }
        return convertToSignableRequest(
                request.getMethod(),
                endpoint,
                resourcePath,
                headersInternal,
                parametersInternal,
                content);
    }
    @Override
    protected void attachSignature(final HttpUriRequest request, final NeptuneSigV4Signature signature)
            throws NeptuneSigV4SignerException {
        // make sure the request is not null and contains the minimal required set of information
        checkNotNull(signature, "The signature must not be null");
        checkNotNull(signature.getHostHeader(), "The signed Host header must not be null");
        checkNotNull(signature.getXAmzDateHeader(), "The signed X-AMZ-DATE header must not be null");
        checkNotNull(signature.getAuthorizationHeader(), "The signed Authorization header must not be null");
        final Header[] headers = request.getAllHeaders();
        // Check if host header is present in the request headers.
        Optional hostHeaderName = Optional.empty();
        for (final Header header: headers) {
            if (header.getName().equalsIgnoreCase(HOST)) {
                hostHeaderName = Optional.of(header.getName());
            }
        }
        // Remove the host header from the request as we are going to add the host header from the signed request.
        // This also ensures that the right header name is used.
        hostHeaderName.ifPresent(request::removeHeaders);
        request.setHeader(HOST, signature.getHostHeader());
        request.setHeader(X_AMZ_DATE, signature.getXAmzDateHeader());
        request.setHeader(AUTHORIZATION, signature.getAuthorizationHeader());
        // https://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
        // For temporary security credentials, it requires an additional HTTP header
        // or query string parameter for the security token. The name of the header
        // or query string parameter is X-Amz-Security-Token, and the value is the session token.
        if (!signature.getSessionToken().isEmpty()) {
            request.setHeader(X_AMZ_SECURITY_TOKEN, signature.getSessionToken());
        }
    }
}