
io.getlime.app.statement.client.ExpressStatementClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of express-statement-client Show documentation
Show all versions of express-statement-client Show documentation
Client library for the express statement service
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