From f8f92ff59e42de861b806e94568bf3f1182989de Mon Sep 17 00:00:00 2001 From: Dom Eori <4j6dq2zi8@relay.firefox.com> Date: Sat, 12 Mar 2022 22:53:09 +0900 Subject: [PATCH] [billing] Add billing server --- docs/application.properties | 6 +- .../aqua/sega/billing/BillingController.java | 117 ++++++++++++++++++ .../model/response/BillingResponse.java | 42 +++++++ .../aqua/sega/billing/util/Decoder.java | 28 +++++ .../sega/billing/util/RawCompression.java | 40 ++++++ .../aqua/spring/configuration/Config.java | 80 +++++++++++- .../samnyan/aqua/spring/util/AutoChecker.java | 22 +++- src/main/resources/application.properties | 3 + src/main/resources/billing.der | Bin 0 -> 635 bytes src/main/resources/server.p12 | Bin 0 -> 2338 bytes .../application-testMysql.properties | 2 + .../application-testSqlite.properties | 2 + 12 files changed, 339 insertions(+), 3 deletions(-) create mode 100644 src/main/java/icu/samnyan/aqua/sega/billing/BillingController.java create mode 100644 src/main/java/icu/samnyan/aqua/sega/billing/model/response/BillingResponse.java create mode 100644 src/main/java/icu/samnyan/aqua/sega/billing/util/Decoder.java create mode 100644 src/main/java/icu/samnyan/aqua/sega/billing/util/RawCompression.java create mode 100644 src/main/resources/billing.der create mode 100644 src/main/resources/server.p12 diff --git a/docs/application.properties b/docs/application.properties index c3fc3007..1c45c137 100644 --- a/docs/application.properties +++ b/docs/application.properties @@ -4,9 +4,13 @@ aimedb.server.enable=true aimedb.server.port=22345 +## Billing server setting +billing.server.enable=true +billing.server.port=8443 + ## Server host & port return to client when boot up. ## By default the same address and port from the client connection is returned. -## Please notice DIVA won't work with localhost or 127.0.0.1 +## Please notice most games won't work with localhost or 127.0.0.1 #allnet.server.host=localhost #allnet.server.port=80 diff --git a/src/main/java/icu/samnyan/aqua/sega/billing/BillingController.java b/src/main/java/icu/samnyan/aqua/sega/billing/BillingController.java new file mode 100644 index 00000000..048e46b7 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/billing/BillingController.java @@ -0,0 +1,117 @@ +package icu.samnyan.aqua.sega.billing; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import icu.samnyan.aqua.sega.billing.model.response.BillingResponse; +import icu.samnyan.aqua.sega.billing.util.Decoder; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.KeyFactory; +import java.security.Signature; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; + +@RestController +public class BillingController { + + private static final Logger logger = LoggerFactory.getLogger(BillingController.class); + + private final ObjectMapper mapper = new ObjectMapper(); + + private final ResourceLoader resourceLoader; + + public BillingController(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @PostMapping(value = "/request", produces = "text/plain") + public String powerOn(InputStream dataStream, HttpServletRequest req) throws IOException { + + RSAPrivateKey key = loadBillingKey(); + + byte[] bytes = dataStream.readAllBytes(); + Map reqMap = Decoder.decode(bytes); + + logger.info("Request: Billing, " + mapper.writeValueAsString(reqMap)); + + String keychipId = reqMap.getOrDefault("keychipid", ""); + + BillingResponse resp = new BillingResponse( + 0, + 100, + 1, + "", + 1024, + signWithKey(key, keychipId, 1024), + "1.000", + 66048, // 0x00010200 + signWithKey(key, keychipId, 66048), + 0, + 5, + "000000/0:000000/0:000000/0"); + logger.info("Response: " + mapper.writeValueAsString(resp)); + return resp.toString().concat("\n"); + } + + private String signWithKey(RSAPrivateKey key, String keychipId, int val) { + String result = ""; + + ByteBuffer sigbytes = ByteBuffer.allocate(15); + sigbytes.order(ByteOrder.LITTLE_ENDIAN); + sigbytes.putInt(0, val); + sigbytes.put(4, keychipId.getBytes()); + + Signature sig; + try { + sig = Signature.getInstance("SHA1withRSA"); + sig.initSign(key); + sig.update(sigbytes); + byte[] signedData = sig.sign(); + result = bytesToHex(signedData); + } catch (Exception e) { + logger.error("Failed to sign with billing key, " + e.getMessage()); + } + + return result; + } + + private RSAPrivateKey loadBillingKey() { + RSAPrivateKey billingKey = null; + + Resource keyRes = resourceLoader.getResource("classpath:billing.der"); + byte[] key; + try { + key = FileCopyUtils.copyToByteArray(keyRes.getInputStream()); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + KeySpec keySpec = new PKCS8EncodedKeySpec(key); + billingKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + } catch (Exception e) { + logger.error("Failed to load billing key file, " + e.getMessage()); + } + + return billingKey; + } + + private String bytesToHex(byte[] in) { + final StringBuilder builder = new StringBuilder(); + for(byte b : in) { + builder.append(String.format("%02x", b)); + } + return builder.toString(); + } + +} diff --git a/src/main/java/icu/samnyan/aqua/sega/billing/model/response/BillingResponse.java b/src/main/java/icu/samnyan/aqua/sega/billing/model/response/BillingResponse.java new file mode 100644 index 00000000..a39e07e1 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/billing/model/response/BillingResponse.java @@ -0,0 +1,42 @@ +package icu.samnyan.aqua.sega.billing.model.response; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author samnyan (privateamusement@protonmail.com) + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BillingResponse { + private int result; + private int waittime; + private int linelimit; + private String message; + private int playlimit; + private String playlimitsig; + private String protocolver; + private int nearfull; + private String nearfullsig; + private int fixlogcnt; + private int fixinterval; + private String playhistory; + + @Override + public String toString() { + return "result=" + result + + "&waittime=" + waittime + + "&linelimit=" + linelimit + + "&message=" + message + + "&playlimit=" + playlimit + + "&playlimitsig=" + playlimitsig + + "&protocolver=" + protocolver + + "&nearfull=" + nearfull + + "&nearfullsig=" + nearfullsig + + "&fixlogcnt=" + fixlogcnt + + "&fixinterval=" + fixinterval + + "&playhistory=" + playhistory; + } +} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/sega/billing/util/Decoder.java b/src/main/java/icu/samnyan/aqua/sega/billing/util/Decoder.java new file mode 100644 index 00000000..c0cc206a --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/billing/util/Decoder.java @@ -0,0 +1,28 @@ +package icu.samnyan.aqua.sega.billing.util; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * @author samnyan (privateamusement@protonmail.com) + */ +public class Decoder { + + public static Map decode(byte[] src) { + //byte[] bytes = Base64.getMimeDecoder().decode(src); + + byte[] output = RawCompression.decompress(src); + + String outputString = new String(output, StandardCharsets.UTF_8).trim(); + String[] split = outputString.split("&"); + Map resultMap = new HashMap<>(); + for (String s : + split) { + String[] kv = s.split("="); + resultMap.put(kv[0], kv[1]); + } + + return resultMap; + } +} diff --git a/src/main/java/icu/samnyan/aqua/sega/billing/util/RawCompression.java b/src/main/java/icu/samnyan/aqua/sega/billing/util/RawCompression.java new file mode 100644 index 00000000..700fe3d1 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/billing/util/RawCompression.java @@ -0,0 +1,40 @@ +package icu.samnyan.aqua.sega.billing.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +import icu.samnyan.aqua.sega.util.ByteBufUtil; + +/** + * @author samnyan (privateamusement@protonmail.com) + */ +public class RawCompression { + + public static byte[] decompress(byte[] src) { + ByteBuf result = Unpooled.buffer(); + byte[] buffer = new byte[100]; + Inflater decompressor = new Inflater(true); // Enable no wrap option + decompressor.setInput(src); + + try { + while (!decompressor.finished()) { + int count = decompressor.inflate(buffer); + if (count == 0) { + break; + } + result.writeBytes(buffer, result.readerIndex(), count); + } + decompressor.end(); + + return ByteBufUtil.toBytes(result); + } catch (DataFormatException e) { + e.printStackTrace(); + return new byte[0]; + } + + } + +} diff --git a/src/main/java/icu/samnyan/aqua/spring/configuration/Config.java b/src/main/java/icu/samnyan/aqua/spring/configuration/Config.java index 82b5e29e..8732016f 100644 --- a/src/main/java/icu/samnyan/aqua/spring/configuration/Config.java +++ b/src/main/java/icu/samnyan/aqua/spring/configuration/Config.java @@ -1,20 +1,98 @@ package icu.samnyan.aqua.spring.configuration; +import java.net.URL; +import java.util.Arrays; + +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; +import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.multipart.commons.CommonsMultipartResolver; - /** * @author samnyan (privateamusement@protonmail.com) */ @Configuration public class Config { + private final int SERVER_PORT; + private final boolean ENABLE_BILLING; + private final int BILLING_PORT; + + public Config(@Value("${server.port}") int SERVER_PORT, + @Value("${billing.server.port}") int BILLING_PORT, + @Value("${billing.server.enable}") boolean ENABLE_BILLING) { + this.SERVER_PORT = SERVER_PORT; + this.BILLING_PORT = BILLING_PORT; + this.ENABLE_BILLING = ENABLE_BILLING; + } + @Bean public CommonsMultipartResolver multipartResolver() { CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); multipartResolver.setMaxUploadSize(-1); return multipartResolver; } + + @Bean + public WebServerFactoryCustomizer webServerFactoryCustomizer() { + + return new WebServerFactoryCustomizer() { + + @Override + public void customize(JettyServletWebServerFactory factory) { + + factory.addServerCustomizers(new JettyServerCustomizer() { + + @Override + public void customize(Server server) { + + ServerConnector httpConnector = new ServerConnector(server); + httpConnector.setPort(SERVER_PORT); + + if (ENABLE_BILLING) { + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + + // TLS_RSA_* ciphers must be enabled, otherwise Auth NG + String[] excludedCiphersWithoutTlsRsaExclusion = Arrays + .stream(sslContextFactory.getExcludeCipherSuites()) + .filter(cipher -> !cipher.equals("^TLS_RSA_.*$")).toArray(String[]::new); + + URL keystoreURL = getClass().getClassLoader().getResource("server.p12"); + sslContextFactory.setKeyStoreResource(Resource.newResource(keystoreURL)); + sslContextFactory.setKeyStorePassword("aquaserver"); + sslContextFactory.setCertAlias("ib"); + sslContextFactory.setExcludeCipherSuites(excludedCiphersWithoutTlsRsaExclusion); + + HttpConfiguration httpsConfiguration = new HttpConfiguration(); + httpsConfiguration.addCustomizer(new SecureRequestCustomizer()); + + ServerConnector httpsConnector = new ServerConnector(server, + new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()), + new HttpConnectionFactory(httpsConfiguration)); + httpsConnector.setPort(BILLING_PORT); + + server.setConnectors(new Connector[] { httpConnector, httpsConnector }); + } else { + server.setConnectors(new Connector[] { httpConnector }); + } + + } + }); + + } + }; + } } diff --git a/src/main/java/icu/samnyan/aqua/spring/util/AutoChecker.java b/src/main/java/icu/samnyan/aqua/spring/util/AutoChecker.java index b2c50eda..e053abc1 100644 --- a/src/main/java/icu/samnyan/aqua/spring/util/AutoChecker.java +++ b/src/main/java/icu/samnyan/aqua/spring/util/AutoChecker.java @@ -27,19 +27,26 @@ public class AutoChecker { private final String AIMEDB_BIND; private final int AIMEDB_PORT; private final boolean AIMEDB_ENABLED; + private final boolean BILLING_ENABLED; + private final int BILLING_PORT; + public AutoChecker( @Value("${server.host:}") String SERVER_PORT, @Value("${allnet.server.host:}") String ALLNET_HOST, @Value("${allnet.server.port:}") String ALLNET_PORT, @Value("${aimedb.server.address}") String AIMEDB_BIND, @Value("${aimedb.server.port}") int AIMEDB_PORT, - @Value("${aimedb.server.enable}") boolean AIMEDB_ENABLED) { + @Value("${aimedb.server.enable}") boolean AIMEDB_ENABLED, + @Value("${billing.server.port}") int BILLING_PORT, + @Value("${billing.server.enable}") boolean BILLING_ENABLED) { this.SERVER_PORT = SERVER_PORT; this.ALLNET_HOST_OVERRIDE = ALLNET_HOST; this.ALLNET_PORT_OVERRIDE = ALLNET_PORT; this.AIMEDB_BIND = AIMEDB_BIND; this.AIMEDB_PORT = AIMEDB_PORT; this.AIMEDB_ENABLED = AIMEDB_ENABLED; + this.BILLING_PORT = BILLING_PORT; + this.BILLING_ENABLED = BILLING_ENABLED; } public void check() { @@ -70,6 +77,19 @@ public class AutoChecker { } } + // Check billing + System.out.print(" Billing : "); + if(!BILLING_ENABLED) { + System.out.println("DISABLED, SKIP"); + } else { + String host = ALLNET_HOST_OVERRIDE.equals("") ? "127.0.0.1" : ALLNET_HOST_OVERRIDE; + try (Socket test = new Socket(host, BILLING_PORT)){ + System.out.println("OK"); + } catch (Exception e) { + System.out.println("ERROR!!"); + System.out.println(e.getMessage()); + } + } // Check http part System.out.print(" AllNet : "); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cda5ed2a..1264fac7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,6 +2,9 @@ aimedb.server.enable=true aimedb.server.address=0.0.0.0 aimedb.server.port=22345 +## Billing server setting +billing.server.enable=true +billing.server.port=8443 ## Server host & port return to client when boot up. ## By default the same address and port from the client connection is returned. ## Please notice DIVA won't work with localhost or 127.0.0.1 diff --git a/src/main/resources/billing.der b/src/main/resources/billing.der new file mode 100644 index 0000000000000000000000000000000000000000..bcd2e89f64a84eb7cc29b54718565889677ca77c GIT binary patch literal 635 zcmV->0)+iAf&zB}0RS)!1_>&LNQUpVK9OMT>=3B0)c@5!uA;TF3$ap z;kE+C*EwyJv!YSOSZADTg(WnE|xQQICDU0>{k#j zc67~SCe=5~7p*nJ`23*mt0s{d60Rn-60E;*egl+?qo=}pPlLR-{AA8d(k3rt2PTHGf zj}X`VFArVr&pCRwO7{eyfjV9d#;3x}N0UJTyJ7I1_J7h+qW#ZGA%YruIFo3$vv|?D zSUc!Ziw?#|{M}u*h3rmEmcOLbUL!2&@5 z^gJ|P+r+DeiDk+GnMm#S*0vjB8WSSRLtT}!L;SxzVvWGj++i`uRB4l}oOS?}GOt@n zhX~?332Y;lHA9d60zm-GT17p}HLv_&NL)dAirhBGmSMG8j@5A++j;KBvS>sbA7u#t3jTFEpqOLj;?W|*$CM6PY>1hz_djt0Kq)N|LqOO1cNZxG5}WQYxMARz<3%(c zw3sRB_yGV`nVHD`P9su#n?OTE+-I8bsN(Q z;0Lpe62=EWh#X#xl*Ok$8(KUpw}~r%IpP{|pqQkSOH|0$8y$YciU#(!+kq6ZF3lF_ znMvUSK>)LqRl{%?p$T^wNRSUAxk*IGYD$*iARR+aIIF_mh!LP24#xPS%wSx*Ui@3t!&~Aaj4<-o9rgeK literal 0 HcmV?d00001 diff --git a/src/main/resources/server.p12 b/src/main/resources/server.p12 new file mode 100644 index 0000000000000000000000000000000000000000..d6552f2562b3618c87b91c5b5cc040a51a1ade71 GIT binary patch literal 2338 zcmY+Ec{me}AIHsR=2~JaM((3IH@TW}<%lA;h?=8ij`F23YMGJi>sqK>MJD9RJ@*j` z5hF!z=4|Lo62-5c=lA>d`~C5GKF{-dKhOKW*BeFQ5@QDdQ547mm`fq*eAG8S04E@a z0(lRjK!$$deiQ|G>rVvEp#ZVJkPAD2?boyalK@bnVD5i6@B+XnCY) zhp>?;z?0?jwghzvCUUqM3Y1ownr*7TOyTM2Z&8LC6CX3*4P5V9cOMGPB(axUY{{08 zYHG25qbW0%6$?vxYf^~ef|J7H>Q#N79?KjS?LS_=95Y}DSHM``^95f@TblYLGhCxd zyqKA(BPfvej`+-@RQHK>ArH&XnS3-^zv4ZWu}mj+PgnSQ)g7fVU#>ub=JL=VWPRQ3 zq%YN1c2?%Jd>R)H4RRfii}Z%n9|F3RUIQz<1kc#qHDaNoFpX_TFdZC6Ed$y;AN$1k ze}ojpH&;k<<;o`$+RoftYLLD)U0^SJskA$c#AlK$*wcdiwi{Rlxy||fY&_o?_Bz~73k~4m{@kgr>T_7!=`kiOsQ1jJxbhWRt zZ|8fbGyhn}xc!B=75CKlTT;?79ozXL*N!ArfOqH0*Ad_`OT%+7w%>h20AWyFv4i0u z?gFsIIVtXEEl@QT?hT+=8efW@X!cZB#;}yb5h;%F2%}0+yh{Ja>qUmD6Y>jmun*J3 zue!9{6TLey3Cf}ss4mFxW;>skqh~rx#TW{f^iSIi&?f4Q-_`*v8`$Eke5r}InT{To zfU>$g(a)ukCaZpX1E3WYT6l0!GwC{TVE6BYkn~LrwL}kD8`ukANXAi!d#qJgkT+H4 z^vk+*W_Cm6!%}hBPDKFbd96{+{+u^a*nDsOmnR&-GwLG?-PJ=9ViNi!~cktL*Xz5Q8>>1!g{|> z2h8&y&A0*VITX-3iUL~wKl^~c?IWw$pR3WzFZ^vEhyr>O^k~_p;YOICLl$nU=Bpv> zXD9&^^tH@Tt^gir(umA?^VWHUGk#S$$bUI)E~80cT*bIYAV_1}vnHD_*<}1x@~2** zfxtI@bGg`5yw@T9lhVt-Z$g^N^Y?=~+|>b$ios}tdE=b_xn+S)F&uo-*aLLf*^NED zDmvpbSlgzfQMiGguk zXM6%IZO3(uVb;-k0j4}()JHUG6l&8~$^BT~!*phW8+PmzrJps8FuT>sIJ8lv=AeRh z3^TZAdW!7^5iu!3nBTV^lH3Hfu98K5UMTjy10#;e_t7q9I9?oFRohM=5^iZFh@X9{1 z(`!a6nydKKl*7E+A~t5tZ6=I3i{tl)BwDA8ba#@ad8*#2HXxEu%y-l+D4Yr;I%`Pz zABu=KnJ(HwPj%nB@YJFCZIOh0i}{9#v7#Mi^pivr=^xja=^ce#vq_KK_BnxPdu8q< zY*>C|HdD0iq2-|`Rot)=jmX|f81#-u6oaSsujXthZ1i3>8!YY7^CI#m36A=V8+;Lv zX}SlU0JJ6&4LFf!y`3~R(zGEN8ivS;%q|){qKV{>iFD|pnz`cp(di*VoSm*#;6DWwe6m%0SbHrDs$j-wxYeZS$`&h4|Y!3XC9gyB@|NxlRWUU1VQ zrB{D1I7&NTeEX3>X;jjn{B(}+6E!+}BI9rtCogcnez6O$vqgDbDf&E*VT*b3nhuSLGRL-|#ioWrnjKseJd$iz`AF?_S@N zhExB*K}Tayg8yAdE*Kgt24cgrxuc~2Ksfps2$nB>9o1Dn=;+DoB!n2$exN`ld7;oK yNfZ~DLlFvOM{ux#V3n4E!Pb-KX}q`>moLfFD!deE>L5tjlj#Jv?9=VPr}JOyYC*~X literal 0 HcmV?d00001 diff --git a/src/test/resources/application-testMysql.properties b/src/test/resources/application-testMysql.properties index 6579bfb3..3631dc6c 100644 --- a/src/test/resources/application-testMysql.properties +++ b/src/test/resources/application-testMysql.properties @@ -5,6 +5,8 @@ aimedb.server.port=22345 allnet.server.host=localhost allnet.server.port=80 aimedb.server.address=127.0.0.1 +billing.server.enable=true +billing.server.port=8443 ## Http Server Port server.port=80 spring.flyway.locations=classpath:db/migration/mysql diff --git a/src/test/resources/application-testSqlite.properties b/src/test/resources/application-testSqlite.properties index e5d73690..ac97b15f 100644 --- a/src/test/resources/application-testSqlite.properties +++ b/src/test/resources/application-testSqlite.properties @@ -5,6 +5,8 @@ aimedb.server.port=22345 allnet.server.host=localhost allnet.server.port=80 aimedb.server.address=127.0.0.1 +billing.server.enable=true +billing.server.port=8443 ## Http Server Port server.port=80 spring.datasource.driver-class-name=org.sqlite.JDBC