com.sap.cds.repackaged.audit.client.impl.HttpCommunicator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cds-feature-auditlog-v2 Show documentation
Show all versions of cds-feature-auditlog-v2 Show documentation
Handler to send auditlog messages to AuditLog Service V2
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());
}
}