From bdcac0f49470949b52180ffc3505af6629c6a60d Mon Sep 17 00:00:00 2001 From: yenon Date: Mon, 6 Nov 2023 16:19:07 +0100 Subject: [PATCH] Added ktor site. --- .idea/kotlinc.xml | 6 +++ build.gradle.kts | 14 ++++++ settings.gradle.kts | 3 ++ src/main/java/Main.java | 52 ------------------- src/main/java/Main.kt | 19 +++++++ src/main/java/SearchPage.kt | 89 +++++++++++++++++++++++++++++++++ src/main/java/TrackPage.kt | 42 ++++++++++++++++ src/main/java/Velocidrone.kt | 97 ++++++++++++++++++++++++++++++++++++ 8 files changed, 270 insertions(+), 52 deletions(-) create mode 100644 .idea/kotlinc.xml delete mode 100644 src/main/java/Main.java create mode 100644 src/main/java/Main.kt create mode 100644 src/main/java/SearchPage.kt create mode 100644 src/main/java/TrackPage.kt create mode 100644 src/main/java/Velocidrone.kt 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 + ) + @Serializable + data class Track( + val id:Long, + val scenery_id:Long, + val track_name:String, + val track_type:String, + val playername:String, + val rating:Float, + val total_ratings:Long, + val date:String + ) + + suspend fun getTrackResults(name:String, author:String): Result { + //scenery_id=&playername=&track_name=&track_type=&order_by_date=True&order_by_rating=True&show_beginner=True&show_intermediate=True&show_advanced=True + //scenery_id=&playername=stimpyten&track_name=&track_type=&order_by_date=True&order_by_rating=True&show_beginner=True&show_intermediate=True&show_advanced=True + + val packet = "scenery_id=&playername=$author&track_name=$name&track_type=&order_by_date=True&order_by_rating=True&show_beginner=True&show_intermediate=True&show_advanced=True" + + return Result.success(Json.decodeFromString(decodeVeloBase64(client.post("https://www.velocidrone.com/api/v1/private/user/rated_tracks_list"){ + val string = "post_data="+URLEncoder.encode(String(encodeVeloBase64(packet)), Charsets.UTF_8) + setBody(string) + setAttributes { + headers["Content-Type"]="application/x-www-form-urlencoded" + } + }.body()).getOrThrow())) + } +} \ No newline at end of file