mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-02-09 15:17:26 +08:00
[+] Rewrite billing
This commit is contained in:
138
src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt
Normal file
138
src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package icu.samnyan.aqua.sega.billing
|
||||||
|
|
||||||
|
import ext.toUrl
|
||||||
|
import icu.samnyan.aqua.sega.util.Decoder.decodeBilling
|
||||||
|
import jakarta.annotation.PostConstruct
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import org.eclipse.jetty.http.HttpVersion
|
||||||
|
import org.eclipse.jetty.server.*
|
||||||
|
import org.eclipse.jetty.util.resource.URLResourceFactory
|
||||||
|
import org.eclipse.jetty.util.ssl.SslContextFactory
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
|
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.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.InputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.Signature
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "billing.server")
|
||||||
|
class BillingProps {
|
||||||
|
var enable: Boolean = true
|
||||||
|
var port: Int = 8443
|
||||||
|
}
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class Billing(
|
||||||
|
@param:Value("\${server.port}") private val SERVER_PORT: Int,
|
||||||
|
val resourceLoader: ResourceLoader,
|
||||||
|
val props: BillingProps
|
||||||
|
) {
|
||||||
|
lateinit var billingKey: PrivateKey
|
||||||
|
|
||||||
|
val logger: Logger = LoggerFactory.getLogger(Billing::class.java)
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
fun init() {
|
||||||
|
val keyRes = resourceLoader.getResource("classpath:billing.der")
|
||||||
|
val key = FileCopyUtils.copyToByteArray(keyRes.inputStream)
|
||||||
|
billingKey = KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/request", produces = ["text/plain"])
|
||||||
|
fun powerOn(dataStream: InputStream, req: HttpServletRequest?): String {
|
||||||
|
val bytes = dataStream.readAllBytes()
|
||||||
|
val reqMap = decodeBilling(bytes)
|
||||||
|
|
||||||
|
logger.info("Billing : $reqMap")
|
||||||
|
|
||||||
|
val keychipId = reqMap["keychipid"] ?: ""
|
||||||
|
val resp = mapOf(
|
||||||
|
"result" to 0,
|
||||||
|
"waitTime" to 100,
|
||||||
|
"lineLimit" to 1,
|
||||||
|
"message" to "",
|
||||||
|
"playLimit" to 1024,
|
||||||
|
"playLimitSig" to sign(keychipId, 1024),
|
||||||
|
"protocolVer" to "1.000",
|
||||||
|
"nearFull" to 66048, // 0x00010200
|
||||||
|
"nearFullSig" to sign(keychipId, 66048),
|
||||||
|
"fixLogCnt" to 0,
|
||||||
|
"fixInterval" to 5,
|
||||||
|
"playHistory" to "000000/0:000000/0:000000/0"
|
||||||
|
)
|
||||||
|
logger.info("> Response: $resp")
|
||||||
|
return resp.mapKeys { it.key.lowercase() }.toUrl() + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign the value with billing key
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
private fun sign(keychipId: String, value: Int) = try {
|
||||||
|
Signature.getInstance("SHA1withRSA").run {
|
||||||
|
initSign(billingKey)
|
||||||
|
update(ByteBuffer.allocate(15).apply {
|
||||||
|
order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
putInt(0, value)
|
||||||
|
put(4, keychipId.toByteArray())
|
||||||
|
})
|
||||||
|
sign().toHexString()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("Failed to sign with billing key, " + e.message)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customizer for the Jetty server
|
||||||
|
*/
|
||||||
|
fun getCustomizer() = object : JettyServerCustomizer {
|
||||||
|
override fun customize(server: Server) {
|
||||||
|
ServerConnector(server).use { conn ->
|
||||||
|
conn.port = SERVER_PORT
|
||||||
|
if (!props.enable) {
|
||||||
|
server.connectors = arrayOf<Connector>(conn)
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
|
||||||
|
val sslContext = SslContextFactory.Server().apply {
|
||||||
|
keyStoreResource = URLResourceFactory().newResource(javaClass.classLoader.getResource("server.p12"))
|
||||||
|
keyStorePassword = "aquaserver"
|
||||||
|
certAlias = "ib"
|
||||||
|
isSniRequired = false
|
||||||
|
setExcludeCipherSuites(*excludeCipherSuites.filter { it != "^TLS_RSA_.*$" }.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerConnector(server,
|
||||||
|
SslConnectionFactory(sslContext, HttpVersion.HTTP_1_1.asString()),
|
||||||
|
HttpConnectionFactory(HttpConfiguration().apply {
|
||||||
|
addCustomizer(SecureRequestCustomizer().apply { isSniHostCheck = false })
|
||||||
|
})
|
||||||
|
).use { httpsConnector ->
|
||||||
|
httpsConnector.port = props.port
|
||||||
|
server.connectors = arrayOf<Connector>(conn, httpsConnector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun webServerFactoryCustomizer() = WebServerFactoryCustomizer<JettyServletWebServerFactory> {
|
||||||
|
it.addServerCustomizers(getCustomizer())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package icu.samnyan.aqua.sega.billing;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import icu.samnyan.aqua.sega.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 jakarta.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<String, String> reqMap = Decoder.decodeBilling(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);
|
|
||||||
|
|
||||||
// JDK 11 compatible version of put(int, byte[])
|
|
||||||
for (int i = 0, j = 4; i < keychipId.getBytes().length; i++, j++) {
|
|
||||||
sigbytes.put(j, keychipId.getBytes()[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package icu.samnyan.aqua.sega.billing;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ import org.springframework.context.annotation.Configuration
|
|||||||
import org.springframework.security.config.Customizer
|
import org.springframework.security.config.Customizer
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
import org.springframework.security.web.SecurityFilterChain
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
import org.springframework.web.cors.CorsConfiguration
|
import org.springframework.web.cors.CorsConfiguration
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
|
||||||
@@ -29,4 +31,7 @@ class SecurityConfig {
|
|||||||
.csrf { it.disable() }
|
.csrf { it.disable() }
|
||||||
.authorizeHttpRequests { it.anyRequest().permitAll() }
|
.authorizeHttpRequests { it.anyRequest().permitAll() }
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun passwordEncoder() = BCryptPasswordEncoder()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
package icu.samnyan.aqua.spring.configuration;
|
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpVersion;
|
|
||||||
import org.eclipse.jetty.server.*;
|
|
||||||
import org.eclipse.jetty.util.resource.URLResourceFactory;
|
|
||||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
|
||||||
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.core.env.Environment;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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, Environment env) {
|
|
||||||
this.SERVER_PORT = SERVER_PORT;
|
|
||||||
this.BILLING_PORT = BILLING_PORT;
|
|
||||||
this.ENABLE_BILLING = ENABLE_BILLING;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PasswordEncoder passwordEncoder() {
|
|
||||||
return new BCryptPasswordEncoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public WebServerFactoryCustomizer<JettyServletWebServerFactory> webServerFactoryCustomizer() {
|
|
||||||
|
|
||||||
return new WebServerFactoryCustomizer<>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(JettyServletWebServerFactory factory) {
|
|
||||||
|
|
||||||
factory.addServerCustomizers(new JettyServerCustomizer() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void customize(Server server) {
|
|
||||||
|
|
||||||
try (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");
|
|
||||||
var resFac = new URLResourceFactory();
|
|
||||||
var res = resFac.newResource(keystoreURL);
|
|
||||||
System.out.println(res);
|
|
||||||
sslContextFactory.setKeyStoreResource(res);
|
|
||||||
sslContextFactory.setKeyStorePassword("aquaserver");
|
|
||||||
sslContextFactory.setCertAlias("ib");
|
|
||||||
sslContextFactory.setExcludeCipherSuites(excludedCiphersWithoutTlsRsaExclusion);
|
|
||||||
sslContextFactory.setSniRequired(false);
|
|
||||||
|
|
||||||
HttpConfiguration httpsConfiguration = new HttpConfiguration();
|
|
||||||
var cus = new SecureRequestCustomizer();
|
|
||||||
cus.setSniHostCheck(false);
|
|
||||||
httpsConfiguration.addCustomizer(cus);
|
|
||||||
|
|
||||||
try (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});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user