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

io.getlime.app.statement.client.ExpressStatementClient Maven / Gradle / Ivy

The newest version!
package io.getlime.app.statement.client;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.util.ISO8601Utils;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import io.getlime.app.statement.client.config.ExpressStatementClientConfiguration;
import io.getlime.app.statement.client.config.ExpressStatementSslConfiguration;
import io.getlime.app.statement.client.ssl.SSLContextFactory;
import io.getlime.app.statement.client.ssl.SimpleClientHttpsRequestFactory;
import io.getlime.app.statement.model.base.ErrorException;
import io.getlime.app.statement.model.base.ExpressStatementHeader;
import io.getlime.app.statement.model.base.SignatureFailedException;
import io.getlime.app.statement.model.rest.objects.Error;
import io.getlime.app.statement.model.rest.request.DeleteBankConnectionRequest;
import io.getlime.app.statement.model.rest.request.DeleteConnectionRequest;
import io.getlime.app.statement.model.rest.response.*;
import io.getlime.app.statement.security.ExpressStatementSignature;
import io.getlime.app.statement.security.RequestCanonizationUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.http.client.*;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import javax.net.ssl.SSLContext;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

@Component
public class ExpressStatementClient {

    public static String BASE_URL = "https://service.rychlyvypis.cz";
    public static String CLIENT_VERSION = "1.8.4";

    /**
     * By setting this property to 'true', it is possible to disable incoming response signature validation.
     * This is useful mainly for testing, and should be never set in production.
     */
    private boolean ignoreIncomingSignatures = false;

    /**
     * By setting this property to any custom value', it is possible to point client to other server instance.
     * This is useful mainly for testing against a mock server, or for possible in-house deployments of the
     * service.
     */
    private String customBaseUrl;

    @Autowired
    private ExpressStatementClientConfiguration configuration;

    @Autowired(required = false)
    private ExpressStatementSslConfiguration sslConfiguration;

    private SSLContext sslContext;

    private ObjectMapper getMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.registerModule(new JodaModule());
        return mapper;
    }

    private RestTemplate defaultRestTemplate(final String signatureBaseString, final String privateKeyBase64, final String publicKeyBase64) {

        // Prepare converters
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(getMapper());
        List> converters = new ArrayList<>();
        converters.add(converter);

        // Prepare interceptors
        ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() {

            @Override
            public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
            try {
                // Prepare request data for signatures
                String requestData;
                if (null == signatureBaseString) {
                    requestData = new String(body);
                } else {
                    requestData = signatureBaseString;
                }

                // Compute the request signature
                final ExpressStatementSignature signature = new ExpressStatementSignature();
                ClientHttpRequest clientRequest = (ClientHttpRequest) request;
                String signatureRequest = signature.computeDataSignature(requestData, privateKeyBase64);
                clientRequest.getHeaders().put(ExpressStatementHeader.SIGNATURE, Collections.singletonList(signatureRequest));
                clientRequest.getHeaders().put("User-Agent", Collections.singletonList("ExpressStatement/" + CLIENT_VERSION));

                // Exchange the data
                final ClientHttpResponse response = execution.execute(request, body);

                // Validate response signature
                final InputStream inputStream = response.getBody();
                final String responseData = IOUtils.toString(inputStream, "UTF-8");
                if (!ignoreIncomingSignatures) {
                    String signatureBase64 = response.getHeaders().getFirst(ExpressStatementHeader.SIGNATURE);
                    // ... workaround for SPR-15087 that is present on Cloudflare (lowercasing HTTP headers)
                    if (signatureBase64 == null) {
                        signatureBase64 = response.getHeaders().getFirst(ExpressStatementHeader.SIGNATURE.toLowerCase());
                    }
                    if (signatureBase64 == null) { // there is still no signature, even with best effort attempt to fetch it...
                        throw new SignatureFailedException(responseData, publicKeyBase64, null);
                    }
                    boolean valid = signature.validateDataSignature(responseData, publicKeyBase64, signatureBase64);
                    if (!valid) {
                        throw new SignatureFailedException(responseData, publicKeyBase64, signatureBase64);
                    }
                } else { // Log the situation
                    Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Validation of incoming signature is disabled. Incoming data may not be authentic.");
                }

                // Return a new wrapped response object
                return new ClientHttpResponse() {
                    @Override
                    public HttpStatus getStatusCode() throws IOException {
                        return response.getStatusCode();
                    }

                    @Override
                    public int getRawStatusCode() throws IOException {
                        return response.getRawStatusCode();
                    }

                    @Override
                    public String getStatusText() throws IOException {
                        return response.getStatusText();
                    }

                    @Override
                    public void close() {
                        response.close();
                    }

                    @Override
                    public InputStream getBody() throws IOException {
                        return new ByteArrayInputStream(responseData.getBytes("UTF-8"));
                    }

                    @Override
                    public HttpHeaders getHeaders() {
                        return response.getHeaders();
                    }
                };
            } catch (InvalidKeySpecException | InvalidKeyException | SignatureException e) {
                Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, e.getMessage(), e);
                ErrorException ex = new ErrorException(e.getMessage());
                ex.getErrors().add(new Error("ERROR_KEY_INVALID", "Invalid key or signature input", "Nesprávný formát klíče nebo inicializačních dat podpisu."));
                throw ex;
            }
            }

        };

        // Prepare the REST template
        final RestTemplate template = new RestTemplate(getClientHttpRequestFactory());
        template.setMessageConverters(converters);
        template.setInterceptors(Collections.singletonList(interceptor));
        return template;
    }

    protected ClientHttpRequestFactory getClientHttpRequestFactory() {
        if (sslConfiguration != null) {
            final String trustStore = sslConfiguration.getSslTrustStore();
            if (sslContext == null && (trustStore == null || trustStore.length() < 1)) {
                return new SimpleClientHttpRequestFactory();
            }
            if (sslContext == null) {
                final String trustStorePassword = sslConfiguration.getSslTrustStorePassword();
                sslContext = SSLContextFactory.createSslContext(trustStore, trustStorePassword);
            }
            return new SimpleClientHttpsRequestFactory(sslContext);
        } else {
            return new SimpleClientHttpRequestFactory();
        }
    }

    private URI buildUri(String resourcePath) {
        return buildUri(resourcePath, null);
    }

    private URI buildUri(String resourcePath, Map parameters) {
        final String url = customBaseUrl != null ? customBaseUrl : BASE_URL;
        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url).path(resourcePath);
        if (parameters != null) {
            for (String key : parameters.keySet()) {
                builder.queryParam(key, parameters.get(key));
            }
        }
        return builder.build().encode().toUri();
    }

    private  T httpGet(String resourcePath, Map params, Class responseType, String publicKey) throws ErrorException {
        URI uri = this.buildUri(resourcePath, params);
        RestTemplate restTemplate = defaultRestTemplate(RequestCanonizationUtils.canonizeGetParameters(uri.getQuery()), configuration.getApplicationPrivateKey(), publicKey);
        ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.GET, null, responseType);
        return responseEntity.getBody();
    }

    private  T httpPost(String resourcePath, Object requestObject, Class responseType, String publicKey) throws ErrorException {
        URI uri = this.buildUri(resourcePath);
        RestTemplate restTemplate = defaultRestTemplate(null, configuration.getApplicationPrivateKey(), publicKey);
        ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(requestObject), responseType);
        return responseEntity.getBody();
    }

    /**
     * Initializes the express statement process by a requesting a new express
     * statement identifier ("session ID").
     *
     * @return New response containing a new session ID and a session public key.
     * @throws ErrorException In case error occurs, for example with signature validation.
     * @see CreateStatementAccountResponse
     */
    public CreateStatementAccountResponse initExpressStatement() throws ErrorException {
        Map params = new HashMap<>();
        params.put("appKey", configuration.getApplicationAppKey());
        params.put("timestamp", ISO8601Utils.format(new Date()));
        params.put("nonce", ExpressStatementSignature.generateNonce());
        return httpGet("/api/statement/id", params, CreateStatementAccountResponse.class, configuration.getServerPublicKey());
    }

    /**
     * Get the list of connected banks with given session ID.
     *
     * @param sessionId Session identifier.
     * @param sessionPublicKey A public key of this session, obtained by calling {@link ExpressStatementClient#initExpressStatement()}.
     * @return Response with a list of currently linked banks and list of available banks.
     * @throws ErrorException ErrorException In case error occurs, for example with signature validation.
     * @see GetLinkedAccountListResponse
     */
    public GetLinkedAccountListResponse fetchConnectedBankList(String sessionId, String sessionPublicKey) throws ErrorException {
        Map params = new HashMap<>();
        params.put("appKey", configuration.getApplicationAppKey());
        params.put("timestamp", ISO8601Utils.format(new Date()));
        params.put("nonce", ExpressStatementSignature.generateNonce());
        params.put("sessionId", sessionId);
        return httpGet("/api/statement/bank/list", params, GetLinkedAccountListResponse.class, sessionPublicKey);
    }

    /**
     * Delete a connection to given bank for given session ID.
     *
     * @param sessionId Session identifier.
     * @param bic BIC code of the bank to be disconnected from the statement.
     * @param sessionPublicKey A public key of this session, obtained by calling {@link ExpressStatementClient#initExpressStatement()}.
     * @return OK response with session ID and BIC code in case everything works as expected.
     * @throws ErrorException In case error occurs, for example with signature validation.
     * @see DeleteBankConnectionResponse
     */
    public DeleteBankConnectionResponse deleteAllConnectionsForBank(String sessionId, String bic, String sessionPublicKey) throws ErrorException {
        DeleteBankConnectionRequest requestObject = new DeleteBankConnectionRequest();
        requestObject.setSessionId(sessionId);
        requestObject.setBic(bic);
        requestObject.setAppKey(configuration.getApplicationAppKey());
        requestObject.setTimestamp(ISO8601Utils.format(new Date()));
        requestObject.setNonce(ExpressStatementSignature.generateNonce());
        return httpPost("/api/statement/bank/delete", requestObject, DeleteBankConnectionResponse.class, sessionPublicKey);
    }

    /**
     * Delete connections to all connected banks for given session ID.
     *
     * @param sessionId Session ID.
     * @param sessionPublicKey A public key of this session, obtained by calling {@link ExpressStatementClient#initExpressStatement()}.
     * @return OK response with session ID in case everything works as expected.
     * @throws ErrorException ErrorException In case error occurs, for example with signature validation.
     * @see DeleteConnectionResponse
     */
    public DeleteConnectionResponse deleteAllConnections(String sessionId, String sessionPublicKey)
            throws ErrorException {
        DeleteConnectionRequest requestObject = new DeleteConnectionRequest();
        requestObject.setSessionId(sessionId);
        requestObject.setAppKey(configuration.getApplicationAppKey());
        requestObject.setTimestamp(ISO8601Utils.format(new Date()));
        requestObject.setNonce(ExpressStatementSignature.generateNonce());
        return httpPost("/api/statement/delete", requestObject, DeleteConnectionResponse.class, sessionPublicKey);
    }

    /**
     * Fetch express statement data for all banks that are connected with a
     * given session ID at the moment.
     *
     * @param sessionId Session ID.
     * @param sessionPublicKey A public key of this session, obtained by calling {@link ExpressStatementClient#initExpressStatement()}.
     * @return Express statement data for all connected banks.
     * @throws ErrorException ErrorException In case error occurs, for example with signature validation.
     * @see GetStatementResponse
     */
    public GetStatementResponse getExpressStatement(String sessionId, String sessionPublicKey) throws ErrorException {
        Map params = new HashMap<>();
        params.put("appKey", configuration.getApplicationAppKey());
        params.put("timestamp", ISO8601Utils.format(new Date()));
        params.put("nonce", ExpressStatementSignature.generateNonce());
        params.put("sessionId", sessionId);
        return httpGet("/api/statement/export", params, GetStatementResponse.class, sessionPublicKey);
    }

    // Property getters and setters

    /**
     * Flag indicating if incoming signatures should be ignores (client does not validate data authenticity).
     *
     * !!! DO NOT USE THIS SETTING IN PRODUCTION  !!!
     * @return True if incoming signatures should be ignored, false otherwise.
     */
    public boolean isIgnoreIncomingSignatures() {
        return ignoreIncomingSignatures;
    }

    /**
     * Flag indicating if incoming signatures should be ignores (client does not validate data authenticity).
     *
     * !!! DO NOT USE THIS SETTING IN PRODUCTION  !!!
     * @param ignoreIncomingSignatures True if incoming signatures should be ignored, false otherwise.
     */
    public void setIgnoreIncomingSignatures(boolean ignoreIncomingSignatures) {
        this.ignoreIncomingSignatures = ignoreIncomingSignatures;
    }

    /**
     * Get custom base URL value.
     *
     * @return Custom base URL.
     */
    public String getCustomBaseUrl() {
        return customBaseUrl;
    }

    /**
     * Set custom base URL value.
     *
     * @param customBaseUrl Custom base URL.
     */
    public void setCustomBaseUrl(String customBaseUrl) {
        this.customBaseUrl = customBaseUrl;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy