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 00000000..bcd2e89f Binary files /dev/null and b/src/main/resources/billing.der differ diff --git a/src/main/resources/server.p12 b/src/main/resources/server.p12 new file mode 100644 index 00000000..d6552f25 Binary files /dev/null and b/src/main/resources/server.p12 differ 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