diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..e805548
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index a6c5d7d..e03776a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,5 +1,7 @@
plugins {
id("java")
+ kotlin("jvm") version "1.9.20"
+ kotlin("plugin.serialization") version "1.9.0"
}
group = "org.example"
@@ -12,8 +14,20 @@ repositories {
dependencies {
testImplementation(platform("org.junit:junit-bom:5.9.1"))
testImplementation("org.junit.jupiter:junit-jupiter")
+ implementation(kotlin("stdlib-jdk8"))
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
+ implementation("io.ktor:ktor-server-core-jvm:2.3.5")
+ implementation("io.ktor:ktor-server-cio-jvm:2.3.5")
+ implementation("io.ktor:ktor-server-status-pages-jvm:2.3.5")
+ implementation("io.ktor:ktor-server-default-headers-jvm:2.3.5")
+ implementation("io.ktor:ktor-client-core:2.3.5")
+ implementation("io.ktor:ktor-client-cio:2.3.5")
+ implementation("io.ktor:ktor-server-html-builder:2.3.5")
}
tasks.test {
useJUnitPlatform()
+}
+kotlin {
+ jvmToolchain(11)
}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ff387ab..a35f230 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,2 +1,5 @@
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
+}
rootProject.name = "VeloDecode"
diff --git a/src/main/java/Main.java b/src/main/java/Main.java
deleted file mode 100644
index c2bfa9c..0000000
--- a/src/main/java/Main.java
+++ /dev/null
@@ -1,52 +0,0 @@
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.spec.SecretKeySpec;
-import java.nio.charset.Charset;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.security.Security;
-import java.util.Base64;
-
-public class Main {
- private static final byte[] key = "BatCaveGGevaCtaB".getBytes(Charset.defaultCharset());
- public static String decodeVeloBs(String input) {
- try {
- var encoded = Base64.getDecoder().decode(input);
- Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
- cipher.init(Cipher.DECRYPT_MODE,new SecretKeySpec(key, "AES"));
- return new String(cipher.doFinal(encoded));
- }catch (Exception ignored){}
- return null;
- }
-
- public static String encodeVeloBs(String input){
- try {
- var encoded = input.getBytes();
- Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
- cipher.init(Cipher.ENCRYPT_MODE,new SecretKeySpec(key, "AES"));
- return new String(Base64.getEncoder().encode(cipher.doFinal(encoded)));
- }catch (Exception ignored){}
- return null;
- }
-
- public static void main(String[] args){
- Security.setProperty("crypto.policy", "unlimited");
-
- //URL url = new URL("http://www.velocidrone.com/api/leaderboard/getLeaderBoard");
- //HttpURLConnection veloConnection = (HttpURLConnection) url.openConnection();
- //veloConnection.setDoInput(true);
- //veloConnection.setDoOutput(true);
- //veloConnection.connect();
- //var writer = new OutputStreamWriter(veloConnection.getOutputStream());
- //writer.write("post_data=wQb/V5e6u70LZd5g1C1ZulicB4/KTXGAErDvWW0VdpuxrJf87bxEqN4dq55t2m5t530teJLczZM31JJLVwDMzKx9jvmxQNZ/Vo1abXxWamEpvmRRF1fHw2rOFK98VpWP");
- //var reader = new BufferedReader(new InputStreamReader(veloConnection.getInputStream()));
-
-
- //var baseString = "0afWszJKVVHqYD1vYdg8+z8E0/Jg7QSa/J+OMhUi4TY6+CoMRHLVpH88cg0DnRGaVtn54YlVzChaTgkFVXPKzeAMK8z7AIzg8ZpVPoAJGH1pHFaHM/6ZHX1W+Uakv1MW+FztGI+hgszogU4DcQFWNstBgOD33yBxJlhjhBNj6V0qxfJaX2lG+04pyon9JzPAS7qqsSo2WYNsfzXdHfU0QgkbUo23+8sglAPwHyJZqnLA7FZmtYsIXQSWnljppoDucwuXc2zZdhLzdsIXPJ/WK9VMEafQ9VbE4UrXzdNSM8cclguFk57rUulw0qwJZLDTlV5GoV4wyJj07TVKIMHKUO9nBpYOZNHac7TdAdV8yX1gWvK/dJmhem/YIWgTc/V70MncZ5zSsTXT74/2SDzptTDdeuRws9mcGA7Tk8SCbwxCHEJstoH0OHsmvl6tPaBZjZ3oyaIf7hLv+t1Qx9thm6WJh/hiatrWcCte/TAJz6lbDDMXplP2llEY7Up+sOeaf/LmIpV5YA4LHz0rQUCuqDv/3x7cPdeIW3P+0FXv8kVRq1w+RGKXF9E+KMK7pSGDhBmTmi/+lb+vfeSvm+cqY3gXao6eyyY4nUA1xKTh+nHp16cW7vFNWLSW/OS78Fz4AaUSBIh9N4RitBeRaUZGAX/y5iKVeWAOCx89K0FArqg7/98e3D3XiFtz/tBV7/JFUatcPkRilxfRPijCu6Uhg4QZk5ov/pW/r33kr5vnKmMnY6uIBccjRlbxEHDV/Pxk4yolUn09qYCD/QF8meoxb/YRouOdqcF3dM4YCQFiiHuBAUqWjmmS6ORk2+s6QMZq6rOkhIfX3nQBwCAaFsdGauo1i7w+8JMNAPGgFdBWa5qdmxfQ4koiLIIcAA0ZjeMLVl9odLt/hUntHti9gP9G8S6Z//YlaHcgV7zU+WWHWz8F3HlD3226SRRgtKpkfKBCAaUSBIh9N4RitBeRaUZGAX/y5iKVeWAOCx89K0FArqg7/98e3D3XiFtz/tBV7/JFUatcPkRilxfRPijCu6Uhg4QZk5ov/pW/r33kr5vnKmPOJUtlqH05/GyBWp8GY++ZkoGkBytjKq9jmkXvkvjlSUArwxm9g1VVzl51aKwMXd6G0wrErJZcmwmiIpB2tNqsjH5Ks+/n8n3Q7veteeIq3LoGVfxvLsVfrKyOpKjCKanjSrRYBI2kHucrqJYjAG1CXOHrL0nfciXcIE6SFb/waxeHLNdDlQvi9RlAH9Sc76vvZwaWDmTR2nO03QHVfMl94ZqyVQvQc6b57RFVpqMlH9DJ3Gec0rE10++P9kg86bUw3XrkcLPZnBgO05PEgm8MQhxCbLaB9Dh7Jr5erT2gWbYOhsYV7FEfaVYHAX4BeREWndxjPEeRZbI6nwwK8D+6+BsCXzW25lwYpbTpvTXcVUTCUmBL5LmPZ/7AQpQ/7FNuXcmr3sGDCU6EqoG4E5rlVVXVVCmGwj0eDfRz6/ZOsYaqtgZOpXtP6hTXUXVSYXY=";
- //var baseString = "wQb/V5e6u70LZd5g1C1ZulicB4/KTXGAErDvWW0VdpuxrJf87bxEqN4dq55t2m5t530teJLczZM31JJLVwDMzKx9jvmxQNZ/Vo1abXxWamEpvmRRF1fHw2rOFK98VpWP";
- var baseString = "dv+uxe7a/WVJ2zEC/uXbjeaw7sHDsRrXYe8gBH5VzCI44PFFIZuoHRbeSDNDSkJBGBybgGJogLtg0tT2apZEWJriB98GjeeZV7Gh7qaNXOxOLH9+Snnc9NPpqNqMTRLiGjR1pvvX9rjJXQAF+d2DrMtsB4RsbyKiJBPeWxWiI6jaPtyDG5E6fS/ErOTHQnxDH7JIhFBwTg0p8FVvIBlNoQ==";
- System.out.println(decodeVeloBs(baseString));
- }
-}
diff --git a/src/main/java/Main.kt b/src/main/java/Main.kt
new file mode 100644
index 0000000..3f7f48c
--- /dev/null
+++ b/src/main/java/Main.kt
@@ -0,0 +1,19 @@
+import io.ktor.server.application.*
+import io.ktor.server.cio.*
+import io.ktor.server.engine.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+
+fun main(){
+ val server = embeddedServer(CIO, port = 8080){
+ routing {
+ get("/"){
+ call.respondRedirect("/search")
+ }
+ searchPage()
+ trackPage()
+ }
+ }
+ println("connection to localhost:8080 now possible.")
+ server.start(true)
+}
\ No newline at end of file
diff --git a/src/main/java/SearchPage.kt b/src/main/java/SearchPage.kt
new file mode 100644
index 0000000..70efbe1
--- /dev/null
+++ b/src/main/java/SearchPage.kt
@@ -0,0 +1,89 @@
+import io.ktor.server.application.*
+import io.ktor.server.html.*
+import io.ktor.server.request.*
+import io.ktor.server.routing.*
+import io.ktor.util.pipeline.*
+import kotlinx.html.*
+
+fun Routing.searchPage(){
+ get("/search"){
+ searchPage()
+ }
+
+ post("/search"){
+ val parameters = call.receiveParameters()
+ val author = parameters["author"] ?: ""
+ val track = parameters["track"] ?: ""
+ val results = Velocidrone.getTrackResults(track,author)
+ results.getOrNull()?.let {
+ searchResultPage(it)
+ }
+ }
+}
+
+private suspend fun PipelineContext.searchPage(){
+ call.respondHtml {
+ head {
+ title {
+ +"Velocidrone track search"
+ }
+ }
+ body {
+ form("/search", encType = FormEncType.applicationXWwwFormUrlEncoded, method = FormMethod.post){
+ p{
+ +"Trackname:"
+ textInput(name = "track")
+ }
+ p{
+ +"Author:"
+ textInput(name = "author")
+ }
+ p{
+ submitInput()
+ }
+ }
+ }
+ }
+}
+
+private suspend fun PipelineContext.searchResultPage(trackResults: Velocidrone.TrackResults){
+ call.respondHtml {
+ head {
+ title {
+ +"Search results"
+ }
+ }
+ body {
+ table {
+ thead {
+ tr {
+ td {
+ +"MapID"
+ }
+ td {
+ +"Name"
+ }
+ td {
+ +"Author"
+ }
+ }
+ }
+ trackResults.user_tracks.forEach {
+ tr {
+ td {
+ a("/track/"+it.id){
+ +it.id.toString()
+ }
+ }
+ td {
+ +it.track_name
+ }
+ td {
+ +it.playername
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/TrackPage.kt b/src/main/java/TrackPage.kt
new file mode 100644
index 0000000..0f94e45
--- /dev/null
+++ b/src/main/java/TrackPage.kt
@@ -0,0 +1,42 @@
+import io.ktor.server.application.*
+import io.ktor.server.html.*
+import io.ktor.server.routing.*
+import io.ktor.util.pipeline.*
+import kotlinx.html.*
+
+fun Routing.trackPage() {
+ get("/track/{id}"){
+ call.parameters.get("id")?.toLong()?.let {
+ val result = Velocidrone.getLeaderboardForId(it)
+ result.getOrNull()?.let {
+ trackPage(it)
+ }
+ }
+ }
+}
+
+private suspend fun PipelineContext.trackPage(leaderboard: Velocidrone.Leaderboard){
+ call.respondHtml {
+ head {
+ title {
+ +"Leaderboard"
+ }
+ }
+ body {
+ table {
+ thead {
+ tr {
+ td { +"Time" }
+ td { +"Name" }
+ }
+ }
+ leaderboard.tracktimes.sortedBy { it.lap_time }.forEach {
+ tr {
+ td { +it.lap_time.toString() }
+ td { +it.playername }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/Velocidrone.kt b/src/main/java/Velocidrone.kt
new file mode 100644
index 0000000..f232705
--- /dev/null
+++ b/src/main/java/Velocidrone.kt
@@ -0,0 +1,97 @@
+import io.ktor.client.*
+import io.ktor.client.call.*
+import io.ktor.client.engine.cio.*
+import io.ktor.client.request.*
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import java.net.URLEncoder
+import java.nio.charset.Charset
+import java.util.*
+import javax.crypto.Cipher
+import javax.crypto.spec.SecretKeySpec
+
+object Velocidrone{
+ private val key = "BatCaveGGevaCtaB".toByteArray(Charset.defaultCharset())
+
+ fun decodeVeloBase64(input: ByteArray): Result {
+ val decoded = Base64.getDecoder().decode(input)
+ val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"))
+ return Result.success(String(cipher.doFinal(decoded)))
+ }
+
+ fun encodeVeloBase64(input: String): ByteArray {
+ val encoded: ByteArray = input.toByteArray()
+ val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
+ cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"))
+ return Base64.getEncoder().encode(cipher.doFinal(encoded))
+ }
+
+ val client = HttpClient(CIO)
+
+ @Serializable
+ data class Leaderboard(
+ val success:Boolean,
+ val message_title:String,
+ val message:String,
+ val tracktimes:Array
+ )
+
+ @Serializable
+ data class TrackTime(
+ val lap_time:Float,
+ val playername: String,
+ val model_id: Long,
+ val country: String,
+ val sim_version: String,
+ val device_type: Long
+ )
+
+ suspend fun getLeaderboardForId(id:Long): Result {
+ //track_id=21192&sim_version=1.16&offset=0&count=400&protected_track_value=2&race_mode=6
+ val packet = "track_id=$id&sim_version=1.16&offset=0&count=400&protected_track_value=2&race_mode=6"
+
+ val result = decodeVeloBase64(client.post("https://www.velocidrone.com/api/leaderboard/getLeaderBoard"){
+ val string = "post_data="+URLEncoder.encode(String(encodeVeloBase64(packet)), Charsets.UTF_8)
+ setBody(string)
+ setAttributes {
+ headers["Content-Type"]="application/x-www-form-urlencoded"
+ }
+ }.body())
+
+ return Result.success(Json.decodeFromString(result.getOrThrow()))
+ }
+
+ @Serializable
+ data class TrackResults(
+ val success:Boolean,
+ val detail:String,
+ val user_tracks:Array