Initial build (#1)

* SP-2: Initial implementation

* SP-2: Create trans cmd fixes

* SP-2: Working prototype

* Add unwrap payment tool result

* Clean Readme, rename project in pom, add idea in .gitignore

* Fix with logback appenders, add description
This commit is contained in:
Pavel Popov 2018-07-12 17:49:56 +03:00 committed by GitHub
parent 065d336b77
commit c3e1a35bd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1757 additions and 1 deletions

120
.gitignore vendored Normal file
View File

@ -0,0 +1,120 @@
# Created by .ignore support plugin (hsz.mobi)
### Maven template
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
cmake-build-release/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
.idea/
*.iml
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

47
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,47 @@
#!groovy
build('samsungpay-provider', 'java-maven') {
checkoutRepo()
def serviceName = env.REPO_NAME
def mvnArgs = '-DjvmArgs="-Xmx256m"'
// Run mvn and generate docker file
runStage('Maven package') {
withCredentials([[$class: 'FileBinding', credentialsId: 'java-maven-settings.xml', variable: 'SETTINGS_XML']]) {
def mvn_command_arguments = ' --batch-mode --settings $SETTINGS_XML -P ci ' +
" -Dgit.branch=${env.BRANCH_NAME} " +
" ${mvnArgs}"
if (env.BRANCH_NAME == 'master') {
sh 'mvn deploy' + mvn_command_arguments
} else {
sh 'mvn package' + mvn_command_arguments
}
}
}
def serviceImage;
def imgShortName = 'rbkmoney/' + "${serviceName}" + ':' + '$COMMIT_ID';
getCommitId()
runStage('Build Service image') {
serviceImage = docker.build(imgShortName, '-f ./target/Dockerfile ./target')
}
try {
if (env.BRANCH_NAME == 'master' || env.BRANCH_NAME.startsWith('epic')) {
runStage('Push Service image') {
docker.withRegistry('https://dr.rbkmoney.com/v2/', 'dockerhub-rbkmoneycibot') {
serviceImage.push();
}
// Push under 'withRegistry' generates 2d record with 'long name' in local docker registry.
// Untag the long-name
sh "docker rmi dr.rbkmoney.com/${imgShortName}"
}
}
}
finally {
runStage('Remove local image') {
// Remove the image to keep Jenkins runner clean.
sh "docker rmi ${imgShortName}"
}
}
}

View File

@ -1 +1,2 @@
# samsungpay-provider
# samsungpay-provider
Payment tool provider implementation for samsung pay

211
pom.xml Normal file
View File

@ -0,0 +1,211 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<name>Samsung pay</name>
<parent>
<groupId>com.rbkmoney</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<groupId>com.rbkmoney.provider</groupId>
<artifactId>samsungpay</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.maintainer>Vladimir Pankrashkin &lt;v.pankrashkin@rbkmoney.com&gt;</project.maintainer>
<server.port>8022</server.port>
<server.rest_port>8080</server.rest_port>
<exposed.ports>${server.port} ${server.rest_port}</exposed.ports>
<dockerfile.base.service.tag>22c57470c4fc47161894f036b7cf9d70f42b75f5</dockerfile.base.service.tag>
<damsel.version>1.232-5e54b26</damsel.version>
<shared.resources.version>0.3.1</shared.resources.version>
</properties>
<dependencies>
<!--RBK libs-->
<dependency>
<groupId>com.rbkmoney.woody</groupId>
<artifactId>woody-thrift</artifactId>
<version>1.1.13</version>
</dependency>
<dependency>
<groupId>com.rbkmoney.logback</groupId>
<artifactId>nop-rolling</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>com.rbkmoney.geck</groupId>
<artifactId>common</artifactId>
<version>0.6.9</version>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>shared-resources</artifactId>
<version>${shared.resources.version}</version>
</dependency>
<dependency>
<groupId>com.rbkmoney</groupId>
<artifactId>damsel</artifactId>
<version>${damsel.version}</version>
</dependency>
<!--Thirdparty libs-->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>5.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.4</version>
</dependency>-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.4</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.9.1</version>
</dependency>
<!--Test libs-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
<targetPath>${project.build.directory}</targetPath>
<includes>
<include>Dockerfile</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>${project.build.directory}/maven-shared-archive-resources</directory>
<filtering>true</filtering>
<excludes>
<exclude>Dockerfile</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-remote-resources-plugin</artifactId>
<version>1.5</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-filtering</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<configuration>
<resourceBundles>
<resourceBundle>com.rbkmoney:shared-resources:${shared.resources.version}</resourceBundle>
</resourceBundles>
<attachToMain>false</attachToMain>
<attachToTest>false</attachToTest>
</configuration>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-Dfile.encoding=${project.build.sourceEncoding}</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,16 @@
package com.rbkmoney.provider.samsungpay;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
/**
* Created by vpankrashkin on 04.07.18.
*/
@SpringBootApplication(scanBasePackages = "com.rbkmoney.provider.samsungpay")
@ServletComponentScan
public class SamsungPayApplication {
public static void main(String[] args) {
SpringApplication.run(SamsungPayApplication.class, args);
}
}

View File

@ -0,0 +1,125 @@
package com.rbkmoney.provider.samsungpay.config;
import com.rbkmoney.damsel.payment_tool_provider.PaymentToolProviderSrv;
import com.rbkmoney.provider.samsungpay.iface.decrypt.ProviderHandler;
import com.rbkmoney.provider.samsungpay.service.SPayClient;
import com.rbkmoney.provider.samsungpay.service.SPayService;
import com.rbkmoney.provider.samsungpay.store.SPKeyStore;
import com.rbkmoney.woody.api.flow.WFlow;
import org.apache.catalina.connector.Connector;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by vpankrashkin on 04.07.18.
*/
@Configuration
public class ApplicationConfig {
@Bean
public SPayClient transactionClient(
@Value("${samsung.trans_url_template}") String transactionURLTemplate,
@Value("${samsung.cred_url_template}") String credentialsURLTemplate,
@Value("${samsung.conn_timeout_ms}") int connTimeoutMs,
@Value("${samsung.read_timeout_ms}") int readTimeoutMs,
@Value("${samsung.write_timeout_ms}") int writeTimeoutMs) {
return new SPayClient(transactionURLTemplate, credentialsURLTemplate, connTimeoutMs, readTimeoutMs, writeTimeoutMs);
}
@Bean
public SPKeyStore keyStore(@Value("${keys_path}") String keysPath) {
return new SPKeyStore(keysPath);
}
@Bean
public SPayService transactionService(SPayClient SPayClient, SPKeyStore spKeyStore) {
return new SPayService(SPayClient, spKeyStore);
}
@Bean
public PaymentToolProviderSrv.Iface providerHandler(SPayService sPayService) {
return new ProviderHandler(sPayService);
}
@Bean
public ServletWebServerFactory servletContainer(@Value("${server.rest_port}") int httpPort) {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
Connector connector = new Connector();
connector.setPort(httpPort);
tomcat.addAdditionalTomcatConnectors(connector);
return tomcat;
}
@Bean
public FilterRegistrationBean externalPortRestrictingFilter(@Value("${server.rest_port}") int restPort, @Value("/${server.rest_path_prefix}/") String httpPathPrefix) {
Filter filter = new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (request.getLocalPort() == restPort) {
if (!(request.getServletPath().startsWith(httpPathPrefix) || request.getServletPath().startsWith("/actuator/health"))) {
response.sendError(404, "Unknown address");
return;
}
}
filterChain.doFilter(request, response);
}
};
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.setOrder(-100);
filterRegistrationBean.setName("httpPortFilter");
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean woodyFilter(@Value("${server.rest_port}") int restPort, @Value("/${server.rest_path_prefix}/") String httpPathPrefix) {
WFlow wFlow = new WFlow();
Filter filter = new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (request.getLocalPort() == restPort) {
if (request.getServletPath().startsWith(httpPathPrefix)) {
wFlow.createServiceFork(() -> {
try {
filterChain.doFilter(request, response);
} catch (IOException | ServletException e) {
sneakyThrow(e);
}
}).run();
return;
}
}
filterChain.doFilter(request, response);
}
private <E extends Throwable, T> T sneakyThrow(Throwable t) throws E {
throw (E) t;
}
};
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.setOrder(-50);
filterRegistrationBean.setName("woodyFilter");
filterRegistrationBean.addUrlPatterns(httpPathPrefix+"*");
return filterRegistrationBean;
}
}

View File

@ -0,0 +1,31 @@
package com.rbkmoney.provider.samsungpay.config;
import com.rbkmoney.provider.samsungpay.iface.transaction.DumbRequestTransactionController;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.HashSet;
/**
* Created by vpankrashkin on 04.04.18.
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage(DumbRequestTransactionController.class.getPackage().getName()))
.paths(PathSelectors.any())
.build();
docket.produces(new HashSet(){{add("application/json");}});
docket.forCodeGeneration(true);
return docket;
}
}

View File

@ -0,0 +1,11 @@
package com.rbkmoney.provider.samsungpay.domain;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Created by vpankrashkin on 05.07.18.
*/
public enum AuthMethod {
@JsonProperty("3DS")
Auth3DS
}

View File

@ -0,0 +1,34 @@
package com.rbkmoney.provider.samsungpay.domain;
import com.rbkmoney.damsel.domain.BankCardPaymentSystem;
public enum CardBrand {
VISA("VI", BankCardPaymentSystem.visa),
MASTERCARD("MC", BankCardPaymentSystem.mastercard);
private String id;
private BankCardPaymentSystem paymentSystem;
CardBrand(String id, BankCardPaymentSystem paymentSystem) {
this.id = id;
this.paymentSystem = paymentSystem;
}
public String getId() {
return id;
}
public BankCardPaymentSystem getPaymentSystem() {
return paymentSystem;
}
public static BankCardPaymentSystem findPaymentSystemById(String id) {
for (CardBrand cardBrand : values()) {
if (cardBrand.getId().equalsIgnoreCase(id)) {
return cardBrand.getPaymentSystem();
}
}
return null;
}
}

View File

@ -0,0 +1,23 @@
package com.rbkmoney.provider.samsungpay.domain;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Created by vpankrashkin on 05.07.18.
*/
public class Certificate {
public String usage;
public String alias;
public String content;
@JsonCreator
public Certificate(
@JsonProperty(value = "usage", required = true) String usage,
@JsonProperty(value = "alias", required = true) String alias,
@JsonProperty(value = "content", required = true) String content) {
this.usage = usage;
this.alias = alias;
this.content = content;
}
}

View File

@ -0,0 +1,32 @@
package com.rbkmoney.provider.samsungpay.domain;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Created by vpankrashkin on 05.07.18.
*/
public class CredentialsResponse {
public String deviceId;
public AuthMethod authMethod;
public String last4digits;
public String cardBrand;
public Data3DS data3DS;
public Certificate[] certificates;
@JsonCreator
public CredentialsResponse(
@JsonProperty(value = "wallet_dm_id") String deviceId,
@JsonProperty(value = "method", required = true) AuthMethod authMethod,
@JsonProperty(value = "card_last4digits") String last4digits,
@JsonProperty(value = "card_brand") String cardBrand,
@JsonProperty(value = "3DS") Data3DS data3DS,
@JsonProperty(value = "certificates", required = true) Certificate[] certificates) {
this.deviceId = deviceId;
this.authMethod = authMethod;
this.last4digits = last4digits;
this.cardBrand = cardBrand;
this.data3DS = data3DS;
this.certificates = certificates;
}
}

View File

@ -0,0 +1,29 @@
package com.rbkmoney.provider.samsungpay.domain;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Created by vpankrashkin on 05.07.18.
*/
public class Data3DS {
@JsonProperty(value = "type", required = true)
public String type;
@JsonProperty(value = "version", required = true)
public String version;
@JsonProperty(value = "data", required = true)
public String data;
@JsonCreator
public Data3DS(
@JsonProperty(value = "type", required = true) String type,
@JsonProperty(value = "version", required = true) String version,
@JsonProperty(value = "data", required = true) String data
) {
this.type = type;
this.version = version;
this.data = data;
}
}

View File

@ -0,0 +1,58 @@
package com.rbkmoney.provider.samsungpay.domain;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.rbkmoney.provider.samsungpay.service.ExpDateDeserialiser;
import java.time.LocalDate;
/**
* Created by vpankrashkin on 05.07.18.
*/
public class PData3DS {
public long amount;
public String currencyCode;
public String created;
public String eci;
public String dpan;
public LocalDate expirationDate;
public String cryptogram;
public String cardholder;
@JsonCreator
public PData3DS(
@JsonProperty(value = "amount", required = true) long amount,
@JsonProperty(value = "currency_code", required = true) String currencyCode,
@JsonProperty(value = "utc", required = true) String created,
@JsonProperty(value = "eci_indicator") String eci,
@JsonProperty(value = "tokenPAN", required = true) String dpan,
@JsonDeserialize(using = ExpDateDeserialiser.class)
@JsonProperty(value = "tokenPanExpiration", required = true) LocalDate expirationDate,
@JsonProperty(value = "cryptogram", required = true) String cryptogram,
@JsonProperty(value = "cardholder_name") String cardholder) {
this.amount = amount;
this.currencyCode = currencyCode;
this.created = created;
this.eci = eci;
this.dpan = dpan;
this.expirationDate = expirationDate;
this.cryptogram = cryptogram;
this.cardholder = cardholder;
}
@Override
public String toString() {
return "PData3DS{" +
"amount=" + amount +
", currencyCode='" + currencyCode + '\'' +
", created='" + created + '\'' +
", eci='" + eci + '\'' +
", dpan='" + (dpan == null ? null : "***") + '\'' +
", expirationDate=" + (expirationDate == null ? null : "***") +
", cryptogram='" + (cryptogram == null ? null : "***") + '\'' +
", cardholder='" + (cardholder == null ? amount : "***") + '\'' +
'}';
}
}

View File

@ -0,0 +1,30 @@
package com.rbkmoney.provider.samsungpay.domain;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Created by vpankrashkin on 05.07.18.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class ResultStatus {
public String code;
public String description;
@JsonCreator
public ResultStatus(
@JsonProperty(value = "resultCode", required = true) String code,
@JsonProperty(value = "resultMessage", required = true) String description) {
this.code = code;
this.description = description;
}
@Override
public String toString() {
return "ResultStatus{" +
"code='" + code + '\'' +
", description='" + description + '\'' +
'}';
}
}

View File

@ -0,0 +1,82 @@
package com.rbkmoney.provider.samsungpay.iface.decrypt;
import com.rbkmoney.damsel.base.InvalidRequest;
import com.rbkmoney.damsel.payment_tool_provider.*;
import com.rbkmoney.provider.samsungpay.domain.CardBrand;
import com.rbkmoney.provider.samsungpay.domain.CredentialsResponse;
import com.rbkmoney.provider.samsungpay.domain.PData3DS;
import com.rbkmoney.provider.samsungpay.service.SPayService;
import org.apache.thrift.TException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
/**
* Created by vpankrashkin on 04.07.18.
*/
public class ProviderHandler implements PaymentToolProviderSrv.Iface {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final SPayService service;
public ProviderHandler(SPayService service) {
this.service = service;
}
@Override
public UnwrappedPaymentTool unwrap(WrappedPaymentTool paymentTool) throws InvalidRequest, TException {
log.info("New unwrap request: {}", paymentTool);
if (!paymentTool.getRequest().isSetSamsung()) {
throw new InvalidRequest(Arrays.asList("Received request type is not SamsungPay"));
}
String refId = paymentTool.getRequest().getSamsung().getReferenceId();
String srvId = paymentTool.getRequest().getSamsung().getServiceId();
try {
Map.Entry<CredentialsResponse, PData3DS> responseWithPData3DS = service.getCredentials(srvId, refId);
CredentialsResponse credentialsResponse = responseWithPData3DS.getKey();
PData3DS pData = responseWithPData3DS.getValue();
log.info("Payment data decrypted: {}", pData);
UnwrappedPaymentTool result = new UnwrappedPaymentTool();
SamsungPayDetails samsungPayDetails = new SamsungPayDetails();
samsungPayDetails.setDeviceId(credentialsResponse.deviceId);
result.setDetails(PaymentDetails.samsung(samsungPayDetails));
CardPaymentData cardPaymentData = new CardPaymentData();
TokenizedCard tokenizedCard = new TokenizedCard();
tokenizedCard.setDpan(pData.dpan);
ExpDate expDate = new ExpDate();
expDate.setMonth((byte) pData.expirationDate.getMonth().getValue());
expDate.setYear((short) pData.expirationDate.getYear());
tokenizedCard.setExpDate(expDate);
AuthData authData = new AuthData();
Auth3DS auth3DS = new Auth3DS();
auth3DS.setCryptogram(pData.cryptogram);
auth3DS.setEci(pData.eci);
authData.setAuth3ds(auth3DS);
tokenizedCard.setAuthData(authData);
cardPaymentData.setTokenizedCard(tokenizedCard);
result.setPaymentData(cardPaymentData);
CardInfo cardInfo = new CardInfo();
// cardInfo.setCardClass(); //?
// cardInfo.setDisplayName(); //?
cardInfo.setLast4Digits(credentialsResponse.last4digits);
cardInfo.setPaymentSystem(CardBrand.findPaymentSystemById(credentialsResponse.cardBrand));
cardInfo.setCardholderName(pData.cardholder);
result.setCardInfo(cardInfo);
return result;
} catch (IOException e) {
//log.error("Failed to read json data: {}", filterPan(e.getMessage()));
throw new InvalidRequest(Arrays.asList("Failed to read json data"));
} catch (Exception e) {
log.error("Failed to get credentials", e);
throw new InvalidRequest(Arrays.asList(e.getMessage()));
}
}
}

View File

@ -0,0 +1,28 @@
package com.rbkmoney.provider.samsungpay.iface.decrypt;
import com.rbkmoney.damsel.payment_tool_provider.PaymentToolProviderSrv;
import com.rbkmoney.woody.thrift.impl.http.THServiceBuilder;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
/**
* Created by vpankrashkin on 18.04.18.
*/
@WebServlet("/provider/samsung")
public class ProviderServlet extends GenericServlet {
private final Servlet handlerServlet;
public ProviderServlet(PaymentToolProviderSrv.Iface handler) {
this.handlerServlet = new THServiceBuilder()
.build(PaymentToolProviderSrv.Iface.class, handler);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
handlerServlet.service(req, res);
}
}

View File

@ -0,0 +1,67 @@
package com.rbkmoney.provider.samsungpay.iface.transaction;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rbkmoney.provider.samsungpay.service.SPayService;
import com.rbkmoney.woody.api.flow.error.WErrorType;
import com.rbkmoney.woody.api.flow.error.WRuntimeException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* Created by vpankrashkin on 04.04.18.
*/
@RestController
@RequestMapping("/api/v1")
@Api(description = "Transaction creation API")
public class DumbRequestTransactionController {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Autowired
private SPayService service;
private ObjectMapper mapper = new ObjectMapper();
@ApiOperation(value = "Request SamsungPay transaction", notes = "")
@PostMapping(value = "/transaction", produces = MediaType.APPLICATION_JSON_UTF8_VALUE, headers = "Content-Type=application/json")
@ApiResponses(value = {
@ApiResponse(code= 200, message = "Samsung Pay session object"),
@ApiResponse(code = 500, message = "Internal service error"),
@ApiResponse(code = 503, message = "Samsung Pay service unavailable")
})
@CrossOrigin
public ResponseEntity<String> getTransaction(@RequestBody Map<String, Object> request) {
log.info("New Transaction request: {}", request);
try {
return ResponseEntity.ok(service.createTransaction(mapper.writeValueAsString(request)));
} catch (WRuntimeException e) {
WErrorType errorType = e.getErrorDefinition().getErrorType();
if (errorType == WErrorType.UNDEFINED_RESULT || errorType == WErrorType.UNAVAILABLE_RESULT) {
log.warn("Samsung pay service unavailable", e);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("Third party service unavailable");
} else {
log.error("Failed to request transaction", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to request transaction");
}
} catch (Exception e) {
log.error("Failed to request transaction", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to request transaction");
}
}
}

View File

@ -0,0 +1,39 @@
package com.rbkmoney.provider.samsungpay.service;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
/**
* Created by vpankrashkin on 05.07.18.
*/
public class Decryptor {
public static String getDecryptedData(String encPayload, PKCS8EncodedKeySpec prvKeySpec) throws Exception {
String delims = "[.]";
String[] tokens = encPayload.split(delims);
Base64.Decoder urlDecoder = Base64.getUrlDecoder();
byte[] encKey = urlDecoder.decode(tokens[1]);
byte[] iv = urlDecoder.decode(tokens[2]);
byte[] cipherText = urlDecoder.decode(tokens[3]);
byte[] tag = urlDecoder.decode(tokens[4]);
byte[] plainText = new byte[cipherText.length];
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privKey = keyFactory.generatePrivate(prvKeySpec);
Cipher decryptCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
decryptCipher.init(Cipher.DECRYPT_MODE, privKey);
byte[] plainEncKey = decryptCipher.doFinal(encKey);
final Cipher aes128Cipher = Cipher.getInstance("AES/GCM/NoPadding");
final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * Byte.SIZE, iv);
final SecretKeySpec keySpec = new SecretKeySpec(plainEncKey, "AES");
aes128Cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
int offset = aes128Cipher.update(cipherText, 0, cipherText.length, plainText, 0);
aes128Cipher.update(tag, 0, tag.length, plainText, offset);
aes128Cipher.doFinal(plainText, offset);
return new String(plainText);
}
}

View File

@ -0,0 +1,32 @@
package com.rbkmoney.provider.samsungpay.service;
/**
* Created by vpankrashkin on 09.07.18.
*/
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class ExpDateDeserialiser extends JsonDeserializer<LocalDate> {
private final DateTimeFormatter fullDateFormatter = DateTimeFormatter.ofPattern("yyMMdd");
private final DateTimeFormatter shortDateFormatter = DateTimeFormatter.ofPattern("MMyy");
@Override
public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
String value = node.asText();
if (value.length() > 4) {
return LocalDate.from(fullDateFormatter.parse(value));
} else {
return LocalDate.from(shortDateFormatter.parse(value));
}
}
}

View File

@ -0,0 +1,294 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.rbkmoney.provider.samsungpay.service;
import okhttp3.*;
import okhttp3.internal.http.HttpHeaders;
import okio.Buffer;
import okio.BufferedSource;
import org.slf4j.LoggerFactory;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
/**
* An OkHttp interceptor which logs request and response information. Can be applied as an
* {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain
* OkHttpClient#networkInterceptors() network interceptor}. <p> The format of the logs created by
* this class should not be considered stable and may change slightly between releases. If you need
* a stable logging format, use your own interceptor.
*/
public final class HttpLoggingInterceptor implements Interceptor {
private static final Charset UTF8 = Charset.forName("UTF-8");
private static final org.slf4j.Logger log = LoggerFactory.getLogger(HttpLoggingInterceptor.class);
public enum Level {
/**
* No logs.
*/
NONE,
/**
* Logs request and response lines.
* <p>
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
* }</pre>
*/
BASIC,
/**
* Logs request and response lines and their respective headers.
* <p>
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
* }</pre>
*/
HEADERS,
/**
* Logs request and response lines and their respective headers and bodies (if present).
* <p>
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
* }</pre>
*/
BODY
}
public interface Logger {
void log(String message);
/**
* A {@link Logger} defaults output appropriate for the current platform.
*/
Logger DEFAULT = message -> log.info(message);
}
public HttpLoggingInterceptor() {
this(Logger.DEFAULT);
}
public HttpLoggingInterceptor(Logger logger) {
this.logger = logger;
}
private final Logger logger;
private volatile Level level = Level.BODY;
/**
* Change the level at which this interceptor logs.
*/
public HttpLoggingInterceptor setLevel(Level level) {
if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
this.level = level;
return this;
}
public Level getLevel() {
return level;
}
@Override
public Response intercept(Chain chain) throws IOException {
Level level = this.level;
Request request = chain.request();
if (level == Level.NONE) {
return chain.proceed(request);
}
boolean logBody = level == Level.BODY;
boolean logHeaders = logBody || level == Level.HEADERS;
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
Connection connection = chain.connection();
StringBuilder message = new StringBuilder("--> \n")
.append(request.method())
.append(' ').append(request.url())
.append(connection != null ? " " + connection.protocol() : "").append('\n');
if (!logHeaders && hasRequestBody) {
message.append(" (").append(requestBody.contentLength()).append("-byte body)").append('\n');
}
//logger.log(requestStartMessage);
if (logHeaders) {
if (hasRequestBody) {
// Request body headers are only present when installed as a network interceptor. Force
// them to be included (when available) so there values are known.
if (requestBody.contentType() != null) {
message.append("Content-Type: ").append(requestBody.contentType()).append('\n');
}
if (requestBody.contentLength() != -1) {
message.append("Content-Length: ").append(requestBody.contentLength()).append('\n');
}
}
Headers headers = request.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
String name = headers.name(i);
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
if (!"Authorization".equalsIgnoreCase(name)) {
message.append(name).append(": ").append(headers.value(i)).append('\n');
} else {
message.append(name).append(": ***").append('\n');
}
}
}
if (!logBody || !hasRequestBody) {
message.append("\n--> END ").append(request.method());
} else if (bodyEncoded(request.headers())) {
message.append("\n--> END ").append(request.method()).append(" (encoded body omitted)");
} else {
message.append('\n');
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (isPlaintext(buffer)) {
message.append(buffer.readString(charset));
message.append("\n--> END ").append(request.method())
.append(" (").append(requestBody.contentLength()).append("-byte body)");
} else {
message.append("\n--> END ").append(request.method()).append(" (binary ")
.append(requestBody.contentLength()).append("-byte body omitted)");
}
}
}
logger.log(message.toString());
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
logger.log("<-- HTTP FAILED: " + e);
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
message = new StringBuilder();
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
message.append("<-- \n")
.append(response.code())
.append(response.message().isEmpty() ? "" : " " + response.message())
.append(' ').append(response.request().url())
.append(" (").append(tookMs).append("ms").append((!logHeaders ? ", " + bodySize + " body" : "") + ')').append('\n');
if (logHeaders) {
Headers headers = response.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
message.append(headers.name(i)).append(": ").append(headers.value(i)).append('\n');
}
if (!logBody || !HttpHeaders.hasBody(response)) {
message.append("\n<-- END HTTP");
} else if (bodyEncoded(response.headers())) {
message.append("\n<-- END HTTP (encoded body omitted)");
} else {
message.append('\n');
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (!isPlaintext(buffer)) {
message.append("\n<-- END HTTP (binary ").append(buffer.size()).append("-byte body omitted)");
return response;
}
if (contentLength != 0) {
message.append(buffer.clone().readString(charset));
}
logger.log(message.append("\n<-- END HTTP (").append(buffer.size()).append("-byte body)").toString());
}
}
return response;
}
/**
* Returns true if the body in question probably contains human readable text. Uses a small sample
* of code points to detect unicode control characters commonly used in binary file signatures.
*/
static boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
private boolean bodyEncoded(Headers headers) {
String contentEncoding = headers.get("Content-Encoding");
return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
}
}

View File

@ -0,0 +1,36 @@
package com.rbkmoney.provider.samsungpay.service;
/**
* Created by vpankrashkin on 12.04.18.
*/
public class SPException extends Exception {
private String payload;
public SPException() {
}
public SPException(String message, String payload) {
super(message);
this.payload = payload;
}
public SPException(String message) {
super(message);
}
public SPException(String message, Throwable cause) {
super(message, cause);
}
public SPException(Throwable cause) {
super(cause);
}
public SPException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public String getPayload() {
return payload;
}
}

View File

@ -0,0 +1,113 @@
package com.rbkmoney.provider.samsungpay.service;
import com.rbkmoney.woody.api.flow.error.WErrorDefinition;
import com.rbkmoney.woody.api.flow.error.WRuntimeException;
import com.rbkmoney.woody.api.trace.context.TraceContext;
import com.rbkmoney.woody.thrift.impl.http.error.THTransportErrorMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.UriTemplate;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
/**
* Created by vpankrashkin on 26.06.18.
*/
public class SPayClient {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final int connTimeoutMs;
private final int readTimeoutMs;
private final int writeTimeoutMs;
private final THTransportErrorMapper errorMapper;
private final UriTemplate transactionTemplate;
private final UriTemplate credentialsTemplate;
public SPayClient(String transactionURLTemplate, String credentialsURLTemplate, int connTimeoutMs, int readTimeoutMs, int writeTimeoutMs) {
this.connTimeoutMs = connTimeoutMs;
this.readTimeoutMs = readTimeoutMs;
this.writeTimeoutMs = writeTimeoutMs;
this.transactionTemplate = new UriTemplate(transactionURLTemplate);
this.credentialsTemplate = new UriTemplate(credentialsURLTemplate);
this.errorMapper = new THTransportErrorMapper();
}
public String requestTransaction(String body) throws SPException {
log.info("Create transaction with: {}", body);
try {
OkHttpClient client = prepareClient();
log.debug("Http client prepared");
Request request = preparePostRequest(transactionTemplate.expand(Collections.emptyMap()).toURL(), body);
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
log.warn("Unsuccessful call result");
}
String result = response.body().string();
log.info("Create transaction result: {}", result);
return result;
} catch (IOException e) {
WErrorDefinition errDef = errorMapper.mapToDef(e, TraceContext.getCurrentTraceData().getActiveSpan());
if (errDef != null) {
throw new WRuntimeException(e, errDef);
} else {
throw new WRuntimeException(e, new WErrorDefinition());
}
}
}
public String requestCredentials(String serviceId, String refId) throws Exception {
log.info("Get credentials for srv:{}, ref:{}", serviceId, refId);
try {
OkHttpClient client = prepareClient();
log.debug("Http client prepared");
Request request = prepareGetRequest(credentialsTemplate.expand(new HashMap() {{
put("id", refId);
put("serviceId", serviceId);
}}).toURL());
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new SPException("Unsuccessful call result", response.body().string());
}
String result = response.body().string();
log.info("Credentials result: {}", result);
return result;
} catch (IOException e) {
WErrorDefinition errDef = errorMapper.mapToDef(e, TraceContext.getCurrentTraceData().getActiveSpan());
if (errDef != null) {
throw new WRuntimeException(e, errDef);
} else {
throw new WRuntimeException(e, new WErrorDefinition());
}
}
}
private OkHttpClient prepareClient() {
return new OkHttpClient.Builder()
.connectTimeout(connTimeoutMs, TimeUnit.MILLISECONDS)
.writeTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS)
.readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS)
.addNetworkInterceptor(c ->
c.proceed(
c.request().newBuilder()
.addHeader("X-Request-Id", TraceContext.getCurrentTraceData().getActiveSpan().getSpan().getTraceId())
.build()
)
)
.addInterceptor(new HttpLoggingInterceptor()).build();
}
private Request preparePostRequest(URL url, String body) {
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), body);
return new Request.Builder().url(url).method("POST", requestBody).build();
}
private Request prepareGetRequest(URL url) {
return new Request.Builder().url(url).method("GET", null).build();
}
}

View File

@ -0,0 +1,60 @@
package com.rbkmoney.provider.samsungpay.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rbkmoney.provider.samsungpay.domain.CredentialsResponse;
import com.rbkmoney.provider.samsungpay.domain.PData3DS;
import com.rbkmoney.provider.samsungpay.domain.ResultStatus;
import com.rbkmoney.provider.samsungpay.store.SPKeyStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.AbstractMap;
import java.util.Map;
/**
* Created by vpankrashkin on 03.07.18.
*/
public class SPayService {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private final SPayClient sPayClient;
private final SPKeyStore keyStore;
private final ObjectMapper mapper = new ObjectMapper();
public SPayService(SPayClient SPayClient, SPKeyStore keyStore) {
this.sPayClient = SPayClient;
this.keyStore = keyStore;
}
public String createTransaction(String reqBody) throws Exception {
String respBody = sPayClient.requestTransaction(reqBody);
ResultStatus status = mapper.readValue(respBody, ResultStatus.class);
if (!"0".equals(status.code)) {
log.warn("Unsuccessful SP response code:{}", status);
}
return respBody;
}
public Map.Entry<CredentialsResponse, PData3DS> getCredentials(String serviceId, String refId) throws Exception {
log.info("Get key for service: {}", serviceId);
PKCS8EncodedKeySpec keySpec = keyStore.getKey(serviceId);
if (keySpec == null) {
log.error("Unknown service id: {}", serviceId);
throw new SPException("Not found key for service: " + serviceId);
}
String respBody = sPayClient.requestCredentials(serviceId, refId);
ResultStatus status = mapper.readValue(respBody, ResultStatus.class);
if (!"0".equals(status.code)) {
log.error("Unsuccessful SP response code:" + status, respBody);
throw new SPException("Unsuccessful SP response code", respBody);
}
CredentialsResponse credResp = mapper.readValue(respBody, CredentialsResponse.class);
//optionally, add response validation
String credentials = Decryptor.getDecryptedData(credResp.data3DS.data, keySpec);
log.info("Payment credentials decrypted");
return new AbstractMap.SimpleEntry<>(credResp, mapper.readValue(credentials, PData3DS.class));
}
}

View File

@ -0,0 +1,25 @@
package com.rbkmoney.provider.samsungpay.store;
/**
* Created by vpankrashkin on 10.04.18.
*/
public class CertStoreException extends RuntimeException {
public CertStoreException() {
}
public CertStoreException(String message) {
super(message);
}
public CertStoreException(String message, Throwable cause) {
super(message, cause);
}
public CertStoreException(Throwable cause) {
super(cause);
}
public CertStoreException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@ -0,0 +1,52 @@
package com.rbkmoney.provider.samsungpay.store;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
/**
* Created by vpankrashkin on 09.04.18.
*/
public class SPKeyStore {
private final Path serviceKeyDir;
public SPKeyStore(String serviceKeyDir) {
this.serviceKeyDir = Paths.get(serviceKeyDir);
}
public PKCS8EncodedKeySpec getKey(String serviceId) {
return getKey(serviceKeyDir, serviceId, ".pem");
}
private PKCS8EncodedKeySpec getKey(Path baseDir, String serviceId, String suffix) {
Path certPath = baseDir.resolve(buildCertFileName(serviceId, suffix));
try {
if (!Files.isRegularFile(certPath)) {
certPath = baseDir.resolve(buildCertFileName(null, suffix));
if (!Files.isRegularFile(certPath)) {
return null;
}
}
return getPrivateKey(new String(Files.readAllBytes(certPath)));
} catch (Exception e) {
throw new CertStoreException(e);
}
}
public static PKCS8EncodedKeySpec getPrivateKey(String pemKey) throws GeneralSecurityException {
pemKey = pemKey.replace("-----BEGIN PRIVATE KEY-----\n", "");
pemKey = pemKey.replace("-----END PRIVATE KEY-----", "");
byte[] encoded = Base64.getDecoder().decode(pemKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return keySpec;
}
private String buildCertFileName(String serviceId, String suffix) {
return (serviceId != null ? serviceId : "") + suffix;
}
}

View File

@ -0,0 +1,21 @@
server.port=@server.port@
server.rest_port=@server.rest_port@
server.rest_path_prefix=api/v1
spring.application.name=@project.name@
info.version=@project.version@
info.damsel.version=@damsel.version@
springfox.documentation.swagger.v2.path=/${server.rest_path_prefix}/swag
logback.appenders=FILE
samsung.version=v1
samsung.endpoint=api-ops.mpay.samsung.com
samsung.trans_url_template=https://${samsung.endpoint}/ops/${samsung.version}/transactions
samsung.cred_url_template=https://${samsung.endpoint}/ops/${samsung.version}/transactions/paymentCredentials/{id}?serviceId={serviceId}
samsung.conn_timeout_ms=3000
samsung.read_timeout_ms=3000
samsung.write_timeout_ms=3000
keys_path=

View File

@ -0,0 +1,14 @@
package com.rbkmoney.provider.samsungpay;
import org.junit.Test;
/**
* Created by vpankrashkin on 04.07.18.
*/
public class IntegrationTest {
@Test
public void testCreateTransaction() {
}
}

View File

@ -0,0 +1,56 @@
package com.rbkmoney.provider.samsungpay;
import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.*;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals;
/**
* Created by vpankrashkin on 12.04.18.
*/
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
properties = {"samsung.endpoint=api-ops.stg.mpay.samsung.com"}
)
public class TransactionRunnerTest {
@Value("http://127.0.0.1:${server.rest_port}/${server.rest_path_prefix}/transaction")
private String transactionUrl;
@Value("classpath:transaction_req.json")
private org.springframework.core.io.Resource resource;
RestTemplate restTemplate = new RestTemplate();
@Test
public void testRequestTransaction() throws IOException {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
//headers.add("X-Request-Id", System.currentTimeMillis()+"");
HttpEntity<String> request = new HttpEntity<>(IOUtils.toString(resource.getInputStream(), StandardCharsets.UTF_8), headers);
ResponseEntity<String> response = restTemplate.postForEntity(transactionUrl, request, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
} catch (HttpClientErrorException e) {
System.out.println(e.getResponseBodyAsString());
throw e;
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="info">
<appender-ref ref="CONSOLE"/>
</root>
<logger name="com.rbkmoney" level="ALL"/>
</configuration>

View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDG++zaPXKaUNdD
Qd54g8lhvkBx2CAmO8cgl50Mvk1eptNVjp4KzyfyxkI4aGfi6EAIp2ZvDwUnz5n2
uvvQxSP8j+DUCP+3sZMJ+0LBdUrYINHqdIh4ONwHaIdeLXSmpRTxogsjN0a1nzh1
p+geXmiWaAm774RBwTomVLGZXU9tQbetwAgK7wb057bpKiCoeeULGqIxskMPS7Qv
rM4iVpsDCEFPTKsVt0bNAM4zWXHBGt2glrHsTC6JIiVjX9o+NlIjrODxkYfFV09a
Mg5X0tZb1Bff4RFAHdVUjcoa9469MkKfrQsoATdGg8IHLFJIxj2xn75DlABZMdAA
FpUQ54PNAgMBAAECggEAUCckpliANHcB7x62XbdARHYdgX48nQoRUSihY4O4qLrs
gBc9xD7j6aBBBnXP+w/w00uTMINNYVb3vvJdAyCWOWM/fknNsBIAl9G1dzYnGt5F
kHq2ii7lOrq5ZI9M4N+4iwjqEZpvijODy6kCEFGZMZTg7uoxTUdnhA8zpwJ2Pxy1
eifKHKTQoqBLG2CCqRC5eKtzN01GMNyeuxn/a4JiTCE04T/x/KO12YjqtylKKK/W
RQ2XCTR3EySCwe0FKzIscfWlTlof1vFj/Mi8H2jzky8/cAn87/DWQitIluq9RKzA
XwHuSqCn2fSvGxTRwyW4AsP85qzVgl/kcQROU+aiQQKBgQD/8vBXfuwQzaBzmena
vRwl31UmMK+FiHyPls17SOe69OAWYOtdhdK+/Ik2n6biTOVtN2bv0eytcKsUfoyF
HY8l4y9RddccpQPwVCSyL7WoF8ILMVPZ09tZ4YPYvDef1/9H4ZhBUqdPsOEGMLaT
tb4Z5bQDZpap3Ik5MYHssFvdfQKBgQDHBhRVoVO333NtXETDO4VJjFQdKpkLVjK/
jbkZ8z1X1PKKlM63OnFUw+Hc35RAyhQmCItUTVPk06glKq/yLvt8KBxIrt4muvUJ
QjoTR6EuV5fMgwQimng+CTxSpuAWd1P/mmvRsUmM3qRI40VWOze+FhERYtqtgkJ8
1CPEhshQkQKBgHqtQq1tVFCpfmJqP0Bsq/UrGnD3nOlwBeP87/hLdWaSwGV5htaI
sf6ApHPeCesl2EGE8H26LKrk+dsU3N1g0Z/jSGbPCI/eOAkVC5GsdHFhEcyzk/Ew
Lk7iXIOhkze2G9GkO4nzx+XWbcS9zIT382oOQz3uCgDYh502MYP089MxAoGAGDG+
ARbmlYC9iHriBRXUQzei9hS6nC0zaCPzb5spRuclQQGMC6w4IMTbTT2EyUeHoYQC
ZBIuc6/jTfldgESD3/kETzWq3ex2Y1TAuJ1Jk9ekJYF73DUJDwmSYr3UmgaRmI4O
M2So+04JjK7MPApg4WPPWVy6FsOyD3i+jGKSMXECgYBXHXavaZC/kru3O1BUr+Ff
g4ttAWzKFJ+9ydgGTzz8HuPY86H7m2Su6U4vdShgW7AieGmK3N9upsNJ/eFuD4/g
SAA/V11lvyHUngYlUFO7OMvU680TLc5U09/CFtymHnApaEXLs6x0LdmbrG5NATfw
KxnXXwDtV+PPkt9bFidQBg==
-----END PRIVATE KEY-----

View File

@ -0,0 +1,20 @@
{
"callback": "0",
"paymentDetails": {
"service": {
"id": "bb12a068ebca4916a7c3c1"
},
"orderNumber": "2346",
"protocol": {
"type": "3DS",
"version": "80"
},
"amount": {
"currency": "RUB",
"total": 98.76
},
"merchant": {
"name": "rbk_merch"
}
}
}

View File

@ -0,0 +1,11 @@
{
"resultCode": "0",
"resultMessage": "SUCCESS",
"id": "cb69dbe93a544622a82f1f",
"href": "https://us-online.stg.mpay.samsung.com/onlinepay",
"encInfo": {
"mod": "a158f2b516930ba7e7f2e47b92753e688cf3d17a796f3d47e7a97eb4b669ca8ab9c3ff3f8d3b910a4f1b134a9575cfb6f33c4f756782acc22ea14a1829b43fd8bbcc4342ea611a623ae494e743b101d64a8f5ef7108fd881348e7f766ab75fb17430d80cdd90264587114019029e416723d454bf8d6229b05b32b66718659c0c78aa9b41156c2a5ada6087717d63075e2adf3e350750b3c285acac62d12a0113982a92252a79a3f7def005b96cfcf8828a815a244275dccd24ed50510756224ffee83b409e5e8466723a7b807a6450f37d151bcbfefa6c1e276097b28144eb345ee886b73f4d28ffd0aba10de76acda3e26fb26e7f3d48e3bc0ea43f5315f873",
"exp": "10001",
"keyId": "52c124f6eb7037a83bf0be70c53ce50ec4d0227bc58468b688d46cc8c7a33fa77e66448eea724f8e0a016607dc0bfaf58b448235c9d1b921b637bf93fe14dfee_310228a3ce3542558f3b"
}
}