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

com.sap.cloud.sdk.s4hana.connectivity.HttpRequestExecutor Maven / Gradle / Ivy

package com.sap.cloud.sdk.s4hana.connectivity;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.zip.GZIPOutputStream;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.RequestLine;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ByteArrayEntity;
import org.slf4j.Logger;

import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.sap.cloud.sdk.cloudplatform.auditlog.AccessedAttribute;
import com.sap.cloud.sdk.cloudplatform.auditlog.AuditLogger;
import com.sap.cloud.sdk.cloudplatform.auditlog.AuditedDataObject;
import com.sap.cloud.sdk.cloudplatform.auditlog.AuditedDataSubject;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.Header;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpEntityUtil;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
import com.sap.cloud.sdk.cloudplatform.exception.ShouldNotHappenException;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.cloudplatform.security.Authorization;
import com.sap.cloud.sdk.s4hana.connectivity.exception.AccessDeniedException;
import com.sap.cloud.sdk.s4hana.connectivity.exception.CloudConnectorException;
import com.sap.cloud.sdk.s4hana.connectivity.exception.LogonErrorException;
import com.sap.cloud.sdk.s4hana.connectivity.exception.MissingConfigException;
import com.sap.cloud.sdk.s4hana.connectivity.exception.QueryExecutionException;
import com.sap.cloud.sdk.s4hana.connectivity.exception.QuerySerializationException;
import com.sap.cloud.sdk.s4hana.serialization.SapClient;

import lombok.Getter;
import lombok.Value;

/**
 * A collection of methods which are commonly called during executions of a query.
 *
 * @param 
 *            The type of the query to execute.
 * @param 
 *            The type of the result to return.
 */
public class HttpRequestExecutor, QueryResultT extends QueryResult>
{
    private static final Logger logger = CloudLoggerFactory.getLogger(HttpRequestExecutor.class);

    @Getter
    private final QueryExecutionMeasurements measurements = new QueryExecutionMeasurements();

    private static class ErpAuthorization extends Authorization
    {
        ErpAuthorization( @Nonnull final String missingAuthorization )
        {
            super(missingAuthorization);
        }
    }

    /**
     * A helper class to wrap request body and headers.
     */
    @Value
    static final class RequestBodyWithHeader
    {
        private final List
headers; private final String body; } private void assertValidConfigContext( final ErpConfigContext configContext ) throws MissingConfigException { if( configContext.getDestinationName() == null ) { throw new MissingConfigException( "No destination name configured (is null). " + "Please provide a destination name in " + ErpConfigContext.class.getSimpleName() + "."); } if( configContext.getSapClient() == null ) { throw new MissingConfigException( "No " + SapClient.class.getSimpleName() + " configured (is null). " + "Please set a " + SapClient.class.getSimpleName() + " in property \"" + ErpConfigContext.DEFAULT_SAP_CLIENT_PROPERTY + "\" of destination \"" + configContext.getDestinationName() + "\" or provide the " + SapClient.class.getSimpleName() + " as an explicit argument of " + ErpConfigContext.class.getSimpleName() + "."); } if( configContext.getLocale() == null ) { throw new MissingConfigException( "No " + Locale.class.getSimpleName() + " configured (is null). " + "Please set a " + Locale.class.getSimpleName() + " in property \"" + ErpConfigContext.DEFAULT_LOCALE_PROPERTY + "\" of destination \"" + configContext.getDestinationName() + "\" or provide the " + Locale.class.getSimpleName() + " as an explicit argument of " + ErpConfigContext.class.getSimpleName() + "."); } } private static final int MAX_UNCOMPRESSED_PAYLOAD_LENGTH = 1400; @Nonnull private ByteArrayEntity getBodyAsCompressedEntity( @Nonnull final String body ) throws QuerySerializationException { final ByteArrayEntity entity; final byte[] content; try { content = body.getBytes(Charsets.UTF_8.toString()); } catch( final UnsupportedEncodingException e ) { throw new QuerySerializationException("Failed to to convert payload from String to UTF8 byte[].", e); } if( content.length > MAX_UNCOMPRESSED_PAYLOAD_LENGTH ) { final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try( final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream) ) { gzipOutputStream.write(content); } catch( final IOException e ) { throw new QuerySerializationException("Failed to write to GZIP-compressed stream.", e); } entity = new ByteArrayEntity(outputStream.toByteArray()); entity.setContentEncoding("gzip"); if( logger.isInfoEnabled() ) { logger.info( "Compressed length of ERP query body: " + entity.getContentLength() + " bytes, was " + content.length + " bytes."); } } else { entity = new ByteArrayEntity(content); entity.setContentEncoding(Charsets.UTF_8.toString()); if( logger.isInfoEnabled() ) { logger.info("Length of ERP query body: " + entity.getContentLength() + " bytes."); } } return entity; } @Nonnull private URI appendQueryParam( @Nonnull final URI uri, @Nullable final String queryParamKey, @Nullable final String queryParamValue ) throws QuerySerializationException { final String query = uri.getQuery(); final String queryParam = queryParamKey + "=" + queryParamValue; final String newQuery; if( Strings.isNullOrEmpty(query) ) { newQuery = queryParam; } else { newQuery = query + "&" + queryParam; } try { return new URI( uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), newQuery, uri.getFragment()); } catch( final URISyntaxException e ) { throw new QuerySerializationException(e); } } private void handleHttpStatus( @Nonnull final ErpConfigContext configContext, final int statusCode, @Nullable final String responseBody, @Nonnull final List
responseHeaders ) throws QueryExecutionException { if( statusCode == HttpStatus.SC_OK ) { if( logger.isTraceEnabled() ) { logger.trace( "Query execution finished successfully. Response body: " + responseBody + " Headers: " + getNonSensitiveHeadersAsString(responseHeaders) + "."); } } else { handleHttpError(configContext, statusCode, responseBody, responseHeaders); } } private void handleHttpError( @Nonnull final ErpConfigContext configContext, final int statusCode, @Nullable final String responseBody, @Nonnull final List
responseHeaders ) throws QueryExecutionException { switch( statusCode ) { case HttpStatus.SC_UNAUTHORIZED: handleUnauthorized(responseBody, responseHeaders); return; case HttpStatus.SC_FORBIDDEN: handleForbidden(responseBody, responseHeaders); return; case HttpStatus.SC_INTERNAL_SERVER_ERROR: handleInternalServerError(responseBody, responseHeaders); return; case HttpStatus.SC_SERVICE_UNAVAILABLE: handleServiceUnavailableError(configContext, responseBody, responseHeaders); return; case HttpStatus.SC_BAD_GATEWAY: handleBadGateway(responseBody, responseHeaders); return; default: { final String message = "Query execution failed with status code " + statusCode + ". Response body: " + responseBody + " Headers: " + getNonSensitiveHeadersAsString(responseHeaders) + "."; throw new QueryExecutionException(message); } } } private void handleUnauthorized( @Nullable final String responseBody, @Nonnull final List
responseHeaders ) throws LogonErrorException { final String message = HttpStatus.SC_UNAUTHORIZED + " Unauthorized. The connection attempt was refused. Response body: " + responseBody + " Headers: " + getNonSensitiveHeadersAsString(responseHeaders) + "."; throw new LogonErrorException(message); } @Nullable private String getMissingAuthorization( @Nonnull final List
responseHeaders ) { for( final Header header : responseHeaders ) { if( header.getName().equals("failed-authorization-object") ) { return header.getValue(); } } return null; } private void handleForbidden( @Nullable final String responseBody, @Nonnull final List
responseHeaders ) throws AccessDeniedException { final String prefix = HttpStatus.SC_FORBIDDEN + " Forbidden. "; if( responseBody != null && responseBody.startsWith("CX_FINS_MAP_NO_AUTH_QUERY_EXEC") ) { @Nullable final String missingAuthorization = getMissingAuthorization(responseHeaders); throw AccessDeniedException.raiseMissingAuthorizations( null, missingAuthorization != null ? Collections.singleton(new ErpAuthorization(missingAuthorization)) : null); } final String message = prefix + "Failed to establish a trusted connection to the ERP. This may be caused by " + "a misconfiguration of the SAP Cloud Connector or a misconfiguration " + "of the trust certificate. Response body: " + responseBody + " Headers: " + getNonSensitiveHeadersAsString(responseHeaders) + "."; throw new AccessDeniedException(message); } private void handleInternalServerError( @Nullable final String responseBody, @Nonnull final List
responseHeaders ) throws QueryExecutionException { final String prefix = HttpStatus.SC_INTERNAL_SERVER_ERROR + " Internal Server Error. "; if( responseBody != null && responseBody.contains("ICF") && responseBody.contains("HCPMAPBM") ) { final String message = prefix + "Failed to invoke ICF service. Does the user have authorization HCPMAPBM? " + "Response body: " + responseBody + " Headers: " + getNonSensitiveHeadersAsString(responseHeaders) + "."; throw new AccessDeniedException(message); } final String message = prefix + "Query execution failed with unexpected error. Response body: " + responseBody + " Headers: " + getNonSensitiveHeadersAsString(responseHeaders) + "."; throw new QueryExecutionException(message); } private void handleServiceUnavailableError( @Nonnull final ErpConfigContext configContext, @Nullable final String responseBody, @Nonnull final List
responseHeaders ) throws QueryExecutionException { if( responseBody != null && responseBody.contains("No tunnels subscribed for tunnelId") ) { final String message = HttpStatus.SC_SERVICE_UNAVAILABLE + " Service Unavailable. Failed to connect to ERP system. " + "Please check the configuration of destination '" + configContext.getDestinationName() + "'. In an on-premise setup, ensure that the cloud connector is connected."; throw new CloudConnectorException(message); } else { handleInternalServerError(responseBody, responseHeaders); } } private void handleBadGateway( @Nullable final String responseBody, @Nonnull final List
responseHeaders ) throws QueryExecutionException { if( responseBody != null && responseBody.contains("Unable to open connection to backend system") ) { final String message = HttpStatus.SC_BAD_GATEWAY + " Bad Gateway. Cloud connector failed to open connection to backend system. " + "Is the internal host configured correctly? Response body: " + responseBody + " Headers: " + getNonSensitiveHeadersAsString(responseHeaders) + "."; throw new CloudConnectorException(message); } else { handleInternalServerError(responseBody, responseHeaders); } } /** * Converts the given headers to a String while omitting sensitive headers to avoid leaking them to logs. */ @Nonnull private String getNonSensitiveHeadersAsString( @Nonnull final List
headers ) { final StringBuilder sb = new StringBuilder(); final Iterator
headerIt = headers.iterator(); while( headerIt.hasNext() ) { final Header header = headerIt.next(); final String name = header.getName(); String value = header.getValue(); if( "set-cookie".equalsIgnoreCase(name) || "authorization".equalsIgnoreCase(name) ) { value = "(hidden)"; } sb.append(name).append(": ").append(value).append(headerIt.hasNext() ? ", " : ""); } return sb.toString(); } /** * Serializes the given query, executes it, and the deserializes the response. * * @param configContext * The {@code ErpConfigContext} of this call. * @param query * The {@code Query} to be executed. * @param querySerializer * The {@code QuerySerializer} to be used to write the query and read the response. * @return The body of the response received by the given query. * @throws QuerySerializationException * If the query could not be serialized * @throws QueryExecutionException * If any Exception occured during execution of the query. * @throws DestinationNotFoundException * If the Destination cannot be found. * @throws DestinationAccessException * If the destination is not of type DestinationType.HTTP or there is an issue while accessing * destination information. */ @Nonnull public QueryResultT execute( @Nonnull final ErpConfigContext configContext, @Nonnull final QueryT query, @Nonnull final QuerySerializer querySerializer ) throws QuerySerializationException, QueryExecutionException, DestinationNotFoundException, DestinationAccessException { assertValidConfigContext(configContext); measurements.resetMeasurements(); measurements.setBeginTotal(System.nanoTime()); try { final SerializedQuery serializedQuery = serializeQuery(configContext, query, querySerializer); final String responseBody = execute(configContext, serializedQuery); return deserializeQuery(configContext, query, querySerializer, responseBody); } finally { measurements.setEndTotal(System.nanoTime()); } } @Nonnull private SerializedQuery serializeQuery( @Nonnull final ErpConfigContext configContext, @Nonnull final QueryT query, @Nonnull final QuerySerializer querySerializer ) throws QuerySerializationException, DestinationNotFoundException, DestinationAccessException { final long beginBuildReq = System.nanoTime(); try { return querySerializer.serialize(query); } finally { final long endBuildReq = System.nanoTime(); measurements.addBuildRequestDuration(Duration.ofNanos(endBuildReq - beginBuildReq)); } } @Nonnull private QueryResultT deserializeQuery( @Nonnull final ErpConfigContext configContext, @Nonnull final QueryT query, @Nonnull final QuerySerializer querySerializer, @Nonnull final String responseBody ) throws QuerySerializationException, DestinationNotFoundException, DestinationAccessException { final long beginParseResp = System.nanoTime(); try { final SerializedQueryResult serializedQueryResult = new SerializedQueryResult<>(query, responseBody); return querySerializer.deserialize(serializedQueryResult); } finally { final long endParseResp = System.nanoTime(); measurements.addParseResponseDuration(Duration.ofNanos(endParseResp - beginParseResp)); } } @Nonnull private RequestMethod getRequestMethod( @Nonnull final SerializedQuery serializedQuery ) { return serializedQuery.getRequestMethod(); } @Nonnull protected URI getRequestUri( @Nonnull final ErpConfigContext configContext, @Nonnull final Destination destination, @Nonnull final SerializedQuery serializedQuery ) { return new ServiceUriBuilder().build(destination.getUri(), serializedQuery.getRequestPath()); } @Nonnull private List
getRequestHeaders( final ErpConfigContext configContext, @Nonnull final SerializedQuery serializedQuery ) { final List
requestHeaders = Lists.newArrayList(serializedQuery.getRequestHeaders()); if( configContext != null ) { // add HTTP header "sap-client" if a SAP client is defined final SapClient sapClient = configContext.getSapClient(); if( sapClient != null && !sapClient.isDefault() && !sapClient.isEmpty() ) { requestHeaders.add(new Header(ErpConfigContext.DEFAULT_SAP_CLIENT_PROPERTY, sapClient.getValue())); } // add HTTP header "sap-language" if a language is defined final Locale locale = configContext.getLocale(); if( locale != null && !Strings.isNullOrEmpty(locale.getLanguage()) ) { requestHeaders.add(new Header(ErpConfigContext.DEFAULT_LOCALE_PROPERTY, locale.getLanguage())); } } return requestHeaders; } private HttpUriRequest newRequest( final RequestMethod requestMethod, final URI requestUri ) { switch( requestMethod ) { case GET: return new HttpGet(requestUri); case HEAD: return new HttpHead(requestUri); case POST: return new HttpPost(requestUri); case PUT: return new HttpPut(requestUri); case PATCH: return new HttpPatch(requestUri); case DELETE: return new HttpDelete(requestUri); case OPTIONS: return new HttpOptions(requestUri); default: throw new ShouldNotHappenException("Unsupported request method: " + requestMethod + "."); } } private HttpUriRequest newRequest( @Nonnull final RequestMethod requestMethod, @Nonnull final URI requestUri, @Nonnull final RequestBodyWithHeader bodyWithHeader ) throws QuerySerializationException { final long beginBuildRequest = System.nanoTime(); try { final HttpUriRequest request = newRequest(requestMethod, requestUri); if( request instanceof HttpEntityEnclosingRequest ) { ((HttpEntityEnclosingRequest) request).setEntity(getBodyAsCompressedEntity(bodyWithHeader.body)); } request.setHeader(HttpHeaders.USER_AGENT, "ErpEndpointSCP"); request.setHeader(HttpHeaders.ACCEPT_ENCODING, "gzip"); for( final Header header : bodyWithHeader.headers ) { request.setHeader(header.getName(), header.getValue()); } if( logger.isTraceEnabled() ) { final Thread currentThread = Thread.currentThread(); logger.trace( "Successfully prepared HTTP request for query execution (thread: " + currentThread + ", threat id: " + currentThread.getId() + ") URI: " + requestUri + " Body: " + bodyWithHeader.body + " Headers: " + getNonSensitiveHeadersAsString(bodyWithHeader.headers) + "."); } return request; } finally { measurements.addBuildRequestDuration(Duration.ofNanos(System.nanoTime() - beginBuildRequest)); } } private void logReadAccessAttempt( final Query query, ErpConfigContext context ) { @Nullable final String readAccessData = query.getReadAccessData(); if( readAccessData != null ) { AuditLogger.logDataReadAttempt( new AuditedDataObject(query.getClass().getSimpleName()), new AuditedDataSubject(context.getDestinationName(), context.getSapClient().getValue()), Lists.newArrayList(new AccessedAttribute(readAccessData, AccessedAttribute.Operation.READ))); } } private String getQueryExecutionFailedMessage( final Query query ) { return query.getClass().getSimpleName() + " " + query.getConstructedByMethod() + " failed [" + measurements.getMeasurementsString() + "]"; } private static final Duration DEFAULT_LONG_RUNNING_REQUEST_THRESHOLD = Duration.ofMillis(2000); private void recordExecutionDuration( final SerializedQuery> serializedQuery, final RequestLine requestLine, final StatusLine statusLine, final List
responseHeaders, final String responseBody ) { final Duration executeRequestDuration = measurements.getExecuteRequestDuration(); final Duration longRunningRequestThreshold = serializedQuery.getQuery().getLongRunningRequestThreshold(); final Duration currLongRunningRequestThreshold = longRunningRequestThreshold != null ? longRunningRequestThreshold : DEFAULT_LONG_RUNNING_REQUEST_THRESHOLD; if( executeRequestDuration != null && currLongRunningRequestThreshold.compareTo(executeRequestDuration) < 0 ) { ErpEndpointMonitor.getInstance().trackLongRunningRequest( measurements.getExecuteRequestDuration(), requestLine.toString(), serializedQuery.getRequestBody(), statusLine.toString(), responseHeaders, responseBody.length()); } } /** * Executes the given {@code serializedQuery} as a {@code HttpUriRequest}, returning the body of the * {@code HttpResponse} received. * * @param configContext * The {@code ErpConfigContext} of this call. * @param serializedQuery * The {@code SerializedQuery} to execute. * * @return The body of the response received by the given query. * * @throws QuerySerializationException * If the query could not be serialized. * @throws QueryExecutionException * If any Exception occured during execution of the query. * @throws DestinationNotFoundException * If the Destination cannot be found. * @throws DestinationAccessException * If the destination is not of type DestinationType.HTTP or there is an issue while accessing * destination information. */ @Nonnull public String execute( @Nonnull final ErpConfigContext configContext, @Nonnull final SerializedQuery serializedQuery ) throws QuerySerializationException, QueryExecutionException, DestinationNotFoundException, DestinationAccessException { final QueryT query = serializedQuery.getQuery(); ErpEndpointMonitor.getInstance().incrementErpQueryCount(query); final Destination destination = DestinationAccessor.getDestination(configContext.getDestinationName()); final HttpClient httpClient = HttpClientAccessor.getHttpClient(destination); final RequestMethod requestMethod = getRequestMethod(serializedQuery); final URI requestUri = getRequestUri(configContext, destination, serializedQuery); // resolve request body and header for potentially signed request body final RequestBodyWithHeader bodyWithHeader = getRequestBodyWithHeader(configContext, serializedQuery); final HttpUriRequest request = newRequest(requestMethod, requestUri, bodyWithHeader); HttpResponse response; final List
responseHeaders = new ArrayList<>(); final String responseBody; final long beginExecute = System.nanoTime(); try { if( logger.isDebugEnabled() ) { logger.debug( "Executing " + query.getClass().getSimpleName() + " constructed by: " + query.getConstructedByMethod() + "."); } logReadAccessAttempt(query, configContext); response = httpClient.execute(request); for( final org.apache.http.Header header : response.getAllHeaders() ) { responseHeaders.add(new Header(header.getName(), header.getValue())); } responseBody = HttpEntityUtil.getResponseBody(response); } catch( final QuerySerializationException e ) { if( logger.isDebugEnabled() ) { logger.debug(getQueryExecutionFailedMessage(query), e); } throw e; } catch( final Exception e ) { final String message = getQueryExecutionFailedMessage(query); throw new QueryExecutionException(message, e); } finally { measurements.addExecuteRequestDuration(Duration.ofNanos(System.nanoTime() - beginExecute)); } if( responseBody == null ) { throw new QueryExecutionException("Failed to execute query: no body returned in response."); } recordExecutionDuration( serializedQuery, request.getRequestLine(), response.getStatusLine(), responseHeaders, responseBody); handleHttpStatus(configContext, response.getStatusLine().getStatusCode(), responseBody, responseHeaders); return responseBody; } /** * Returns a wrapper object which encapsulates the HTTP request body and headers. This method can be overridden to * manipulate the request before submitting, e.g. signing queries, adding timestamps. * * @param configContext * The {@code ErpConfigContext} of this call. * @param query * The {@code Query} to be executed. */ @Nonnull protected RequestBodyWithHeader getRequestBodyWithHeader( final ErpConfigContext configContext, @Nonnull final SerializedQuery query ) { return new RequestBodyWithHeader(getRequestHeaders(configContext, query), query.getRequestBody()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy