All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cds.repackaged.audit.client.impl.HttpCommunicator Maven / Gradle / Ivy

There is a newer version: 3.2.0
Show newest version
package com.sap.cds.repackaged.audit.client.impl;

import static com.sap.cds.repackaged.audit.client.impl.Utils.OAUTH2_PLAN;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static org.apache.http.HttpHeaders.AUTHORIZATION;
import static org.apache.http.HttpStatus.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;

import org.apache.http.HttpClientConnection;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sap.cloud.security.config.CredentialType;
import com.sap.cloud.security.mtls.SSLContextFactory;
import com.sap.cloud.security.xsuaa.tokenflows.TokenFlowException;
import com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlows;
import com.sap.cds.repackaged.audit.api.exception.AuditLogWriteException;
import com.sap.cds.repackaged.audit.api.exception.InvalidTokenIssuerException;
import com.sap.xs.env.Credentials;

/**
 * HttpCommunicator is responsible for the connection management and error handling in the audit log
 * client. Connection parameters have sensible defaults which can be overridden via the application
 * manifest or environment.
 */
public class HttpCommunicator implements Communicator {
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpCommunicator.class);

    private final String serviceUrl;
    private final String servicePlan;
    private final Credentials serviceCredentials;
    private final HttpClient httpClient;
    private final OAuthCredentials oauthCredentials;
    private final XsuaaTokenFlowsFactory tokenFlowsFactory;
    private final ConnectionConfigLoader configLoader;

    private HttpClientContext localContext;

    private static final String ERR_MSG_TEMPLATE = "Audit log client received HTTP status code %s from audit service. "
            + "Audit log might not have been written in the audit log storage. Audit service response: \"%s\".";
    private static final String ERR_MSG_RETRY_TEMPLATE = "Audit log client made %s failed attempts to send the audit log message. "
            + "The message might not have been written in the audit log storage. Received HTTP status code is %s and response body: \"%s\".";
    private static final String ERR_MSG_EXC_TEMPLATE =
        "Could not send the audit log message to the audit log service after %s failed connection attempts. Exception is: %s";
    private static final String OBTAINING_TOKEN_ERROR = "Problem occurred while trying to obtain token";

    private static final String REGEX_DOT_SPLIT = "\\.";
    private static final String REGEX_DOT_JOIN = ".";
    private static final String CERT_PATH = "cert";
    private static final int CERT_PATH_INDEX = 1;

    /**
     * This is a constructor of {@link HttpCommunicator} with arguments for the connection
     * management.
     * 
     * @param credentials
     *            These are credentials for a connection to the auditlog server.
     * @param plan
     *            This is a plan used for access control.
     * @param versionedAuditServiceURLPath
     *            This is an endpoint of the auditlog server.
     * @param connConfigJson
     *            These are properties used to configure a connection manager.
     * @param sslContextFactory
     *            This is an instance of the {@linkplain SSLContextFactory} used to create
     *            SSLContext.
     * @param httpClientConnectionManager
     *            This is an instance of {@linkplain PoolingHttpClientConnectionManager} used to
     *            manage a pool of {@link HttpClientConnection} to the auditlog service.
     */
    public HttpCommunicator(Credentials credentials, String plan, String versionedAuditServiceURLPath,
            String connConfigJson, SSLContextFactory sslContextFactory,
            PoolingHttpClientConnectionManager httpClientConnectionManager) {
        this(credentials, new ServiceCredentialParser(credentials).parseCredentials(),
                plan, versionedAuditServiceURLPath, connConfigJson, sslContextFactory, httpClientConnectionManager);
    }

    /**
     * The constructor of {@link HttpCommunicator} with default settings of the connection
     * management.
     * 
     * @param credentials
     *            These are credentials for a connection to the auditlog server.
     * @param plan
     *            This is a plan used for access control.
     * @param versionedAuditServiceURLPath
     *            This is an endpoint of the auditlog server.
     */
    public HttpCommunicator(Credentials credentials, String plan, String versionedAuditServiceURLPath) {
        this(credentials, plan, versionedAuditServiceURLPath, null, SSLContextFactory.getInstance(),
                new PoolingHttpClientConnectionManager());
    }

    public HttpCommunicator(Credentials credentials, String plan, String versionedAuditServiceURLPath,
            SSLContextFactory sslContextFactory, PoolingHttpClientConnectionManager httpClientConnectionManager) {
        this(credentials, plan, versionedAuditServiceURLPath, null, sslContextFactory, httpClientConnectionManager);
    }

    protected HttpCommunicator(Credentials credentials, OAuthCredentials oauthCredentials,
            String plan, String versionedAuditServiceURLPath,
            String connConfigJson, SSLContextFactory sslContextFactory,
            PoolingHttpClientConnectionManager httpClientConnectionManager) {
        this.configLoader = new ConnectionConfigLoader(connConfigJson);

        // Connection pool manager associated with the client
        httpClientConnectionManager.setMaxTotal(configLoader.getHttpPoolMaxConn());
        httpClientConnectionManager.setDefaultMaxPerRoute(configLoader.getHttpPoolMaxConnPerRoute());

        this.serviceCredentials = credentials;
        this.oauthCredentials = oauthCredentials;

        final HttpClientFactory httpClientFactory = new HttpClientFactory(sslContextFactory, oauthCredentials, configLoader);
        this.httpClient = httpClientFactory.createHttpClient(httpClientConnectionManager);
        final HttpClient httpClientWithSSLContext = httpClientFactory.createHttpClientWithSSLContext(httpClientConnectionManager);
        this.tokenFlowsFactory = new XsuaaTokenFlowsFactory(httpClientWithSSLContext);

        this.serviceUrl = versionedAuditServiceURLPath;
        this.servicePlan = plan;
    }

    private String getAuthorizationHeader(String subscriberTokenIssuer)
            throws AuditLogWriteException, InvalidTokenIssuerException {
        return servicePlan.equals(OAUTH2_PLAN) //
                ? getAuthHeaderValueForOAuth2Auth(subscriberTokenIssuer) //
                : getAuthHeaderValueForBasicAuth(); //
    }

    String getAuthHeaderValueForBasicAuth() throws AuditLogWriteException {
        String username = serviceCredentials.getUser();
        String password = serviceCredentials.getPassword();
        String plainCredentials = username + ":" + password;
        String encodedCredentials = Base64.getEncoder()
                .encodeToString(plainCredentials.getBytes(ISO_8859_1));
        return "Basic " + encodedCredentials;
    }

    String getAuthHeaderValueForOAuth2Auth(String subscriberTokenIssuer)
            throws AuditLogWriteException, InvalidTokenIssuerException {
        TokenFactory tokenFactory = getTokenFactory(subscriberTokenIssuer);
        String accessToken = obtainToken(tokenFactory);
        if (accessToken == null) {
            throw new AuditLogWriteException(OBTAINING_TOKEN_ERROR);
        }
        return "Bearer " + accessToken;
    }

    protected TokenFactory getTokenFactory(String subscriberTokenIssuer)
            throws AuditLogWriteException, InvalidTokenIssuerException {
        final OAuthCredentials oauthCredentials = createOAuthCredentials(subscriberTokenIssuer);
        final XsuaaTokenFlows tokenFlows = tokenFlowsFactory.getXsuaaTokenFlows(oauthCredentials);
        return new TokenFactory(tokenFlows);
    }

    private OAuthCredentials createOAuthCredentials(String subscriberTokenIssuer) throws AuditLogWriteException {
        oauthCredentials.setSubscriberTokenIssuer(subscriberTokenIssuer);
        return oauthCredentials;
    }

    String obtainToken(TokenFactory tokenFactory) throws AuditLogWriteException {
        try {
            return tokenFactory.getClientCredentialsGrantAccessToken();
        } catch (TokenFlowException e) {
            throw new AuditLogWriteException(OBTAINING_TOKEN_ERROR, e);
        }
    }

    @Override
    public String send(String message, String endpoint, String subscriberTokenIssuer)
            throws AuditLogWriteException, UnsupportedOperationException {
        try {
            int messageHash = System.identityHashCode(message);
            LOGGER.debug("Persisting audit log ({}): '{}'.", messageHash, message);

            String authorizatioHeader = getAuthorizationHeader(subscriberTokenIssuer);
            HttpPost request = createPostRequest(message, endpoint, authorizatioHeader);
            HttpResponse response = sendAndRetryOnHttpErrorCodes(request);

            String responseBody = consumeResponse(response);

            int statusCode = response.getStatusLine()
                    .getStatusCode();

            LOGGER.debug("Status code ({}): '{}'.", messageHash, statusCode);
            LOGGER.debug("Response ({}): '{}'.", messageHash, responseBody);

            if (statusCode == SC_OK || statusCode == SC_CREATED || statusCode == SC_NO_CONTENT) {
                LOGGER.debug("Persisted audit log ({}).", messageHash);

                return responseBody;
            } else {
                throw new AuditLogWriteException(String.format(ERR_MSG_TEMPLATE, statusCode, responseBody));
            }
        } catch (Exception e) { // Apache HTTP Client throws many RuntimeExceptions
            if (e instanceof AuditLogWriteException) {
                throw (AuditLogWriteException) e;
            } else {
                throw new AuditLogWriteException("Unable to persist audit log!", e);
            }
        }
    }

    HttpPost createPostRequest(String message, String endpoint, String authorizationHeader)
            throws AuditLogWriteException {
        HttpPost request = new HttpPost(endpoint);
        // it was done only once before (in the constructor) because the credentials
        // were always the same - username and pass.
        // But with the 'oauth2' scenario the token could be expired therefore a new one
        // should be requested
        request.setHeader(AUTHORIZATION, authorizationHeader);

        StringEntity params = new StringEntity(message, ContentType.APPLICATION_JSON);
        request.setEntity(params);
        return request;
    }

    private HttpResponse sendAndRetryOnHttpErrorCodes(HttpEntityEnclosingRequestBase request)
            throws AuditLogWriteException {
        HttpResponse lastResponse = null;
        int statusCode = 0;
        String responseBody = null;

        for (int i = 0; i <= configLoader.getMaxRetryCountOnHttpErrorResponse(); i++) {
            if (lastResponse != null) {
                LOGGER.debug(
                    "Received response with HTTP status code: {} and response body: \"{}\". Request will be retried {} more times.",
                    statusCode, responseBody, (configLoader.getMaxRetryCountOnHttpErrorResponse() - i + 1));
                waitBeforeRetry(configLoader.getDelayRetryOnHttpErrorResponse());
            }

            lastResponse = sendAndRetryOnException(request);
            statusCode = lastResponse.getStatusLine()
                    .getStatusCode();

            if (!isRetryOnRC(statusCode)) {
                // stop the loop if RC is successful or cannot be recovered from
                return lastResponse;
            } else {
                // consume the unneeded error responses
                responseBody = consumeResponse(lastResponse);
            }
        }

        throw new AuditLogWriteException(String.format(ERR_MSG_RETRY_TEMPLATE,
            configLoader.getMaxRetryCountOnHttpErrorResponse() + 1, statusCode, responseBody));
    }

    private HttpResponse sendAndRetryOnException(HttpEntityEnclosingRequestBase request) throws AuditLogWriteException {
        HttpResponse response = null;
        Exception exception = null; // this is the last thrown exception

        for (int i = 0; i <= configLoader.getMaxRetryCountOnException(); i++) {
            try {
                response = httpClient.execute(request, localContext);
                exception = null;
                break;
            } catch (IOException e) {
                exception = e;
                LOGGER.warn("Connection attempt failed: {}. Retrying ...", e.getMessage());
            }

            waitBeforeRetry(configLoader.getDelayRetryOnException());
        }

        if (exception != null) {
            String err = String.format(ERR_MSG_EXC_TEMPLATE, configLoader.getMaxRetryCountOnException() + 1, exception.getMessage());
            LOGGER.error(err);
            throw new AuditLogWriteException(err, exception);
        }

        return response;
    }

    private boolean waitBeforeRetry(long waitTime) {
        if (waitTime != 0) {
            try {
                Thread.sleep(waitTime);
            } catch (InterruptedException interrupted) {
                LOGGER.warn("Delay before connection retry for sending an auditlog message was interrupted.");
                return false;
            }
        }

        return true;
    }

    private static String consumeResponse(HttpResponse response) {
        String responseBody = "";
        try {
            HttpEntity entity = null;
            if (response != null && (entity = response.getEntity()) != null) {
                responseBody = EntityUtils.toString(entity);
                // make sure that there are no leaked resources by closing the stream but
                // keeping the connection alive
                EntityUtils.consume(entity);
            }
        } catch (IOException e) {
            LOGGER.error("Cannot consume the response from the connection.", e);
        }

        return responseBody;
    }

    // check if the response code belongs to the group for which we perform retries
    private boolean isRetryOnRC(int rc) {
        return rc == SC_NOT_FOUND || rc == SC_SERVICE_UNAVAILABLE || rc == SC_INTERNAL_SERVER_ERROR
                || rc == SC_BAD_GATEWAY || rc == SC_GATEWAY_TIMEOUT;
    }

    @Override
    public String getClientId() {
        return servicePlan.equals(OAUTH2_PLAN)
                ? this.oauthCredentials.getClientid()
                : null;
    }

    @Override
    public String getServiceUrl() {
        return this.serviceUrl;
    }

    @Override
    public String getServicePlan() {
        return this.servicePlan;
    }

    @Override
    public String getUaaDomain() {
        final String uaaDomain = oauthCredentials.getUaaDomain();
        LOGGER.debug("Getting uaaDomain: ({}).", uaaDomain);
        if (isX509CredentialType()) {
            ArrayList uaaDomainArray = new ArrayList<>(Arrays.asList(uaaDomain
                    .split(REGEX_DOT_SPLIT)));
            uaaDomainArray.add(CERT_PATH_INDEX, CERT_PATH);
            return String.join(REGEX_DOT_JOIN, uaaDomainArray);
        }
        return uaaDomain;
    }

    @Override
    public boolean isX509CredentialType() {
        return CredentialType.X509.toString()
                .equals(oauthCredentials.getCredentialType());
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy