[+] Rewrite billing

This commit is contained in:
Azalea
2024-02-25 21:11:52 -05:00
parent 51a0e46f8c
commit eb960209bf
5 changed files with 143 additions and 259 deletions

View 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())
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -5,6 +5,8 @@ import org.springframework.context.annotation.Configuration
import org.springframework.security.config.Customizer
import org.springframework.security.config.annotation.web.builders.HttpSecurity
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.web.cors.CorsConfiguration
import org.springframework.web.cors.UrlBasedCorsConfigurationSource
@@ -29,4 +31,7 @@ class SecurityConfig {
.csrf { it.disable() }
.authorizeHttpRequests { it.anyRequest().permitAll() }
.build()
@Bean
fun passwordEncoder() = BCryptPasswordEncoder()
}

View File

@@ -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});
}
}
}
});
}
};
}
}