diff --git a/src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt b/src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt new file mode 100644 index 00000000..13997857 --- /dev/null +++ b/src/main/java/icu/samnyan/aqua/sega/billing/Billing.kt @@ -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(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(conn, httpsConnector) + } + } + } + } + + @Bean + fun webServerFactoryCustomizer() = WebServerFactoryCustomizer { + it.addServerCustomizers(getCustomizer()) + } +} diff --git a/src/main/java/icu/samnyan/aqua/sega/billing/BillingController.java b/src/main/java/icu/samnyan/aqua/sega/billing/BillingController.java deleted file mode 100644 index 5c5e599f..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/billing/BillingController.java +++ /dev/null @@ -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 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(); - } - -} diff --git a/src/main/java/icu/samnyan/aqua/sega/billing/BillingResponse.java b/src/main/java/icu/samnyan/aqua/sega/billing/BillingResponse.java deleted file mode 100644 index 63d6c545..00000000 --- a/src/main/java/icu/samnyan/aqua/sega/billing/BillingResponse.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/src/main/java/icu/samnyan/aqua/spring/SecurityConfig.kt b/src/main/java/icu/samnyan/aqua/spring/SecurityConfig.kt index 537b7dd8..eb06828c 100644 --- a/src/main/java/icu/samnyan/aqua/spring/SecurityConfig.kt +++ b/src/main/java/icu/samnyan/aqua/spring/SecurityConfig.kt @@ -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() } diff --git a/src/main/java/icu/samnyan/aqua/spring/configuration/Config.java b/src/main/java/icu/samnyan/aqua/spring/configuration/Config.java deleted file mode 100644 index 757fee54..00000000 --- a/src/main/java/icu/samnyan/aqua/spring/configuration/Config.java +++ /dev/null @@ -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 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}); - } - } - } - }); - - } - }; - } -}