Skip to content

Commit

Permalink
add m202 verification
Browse files Browse the repository at this point in the history
  • Loading branch information
BigBoot committed Oct 20, 2023
1 parent bf76220 commit 5d69661
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 0 deletions.
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ dependencies {
// CopyDown
implementation("io.github.furstenheim:copy_down:1.1")

// Zip4J
implementation("net.lingala.zip4j:zip4j:2.11.5")

// JUnit
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.1")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.1")
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/de/bigboot/ggtools/fang/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import com.squareup.moshi.Moshi
import org.tomlj.Toml
import java.nio.file.Paths

@JsonClass(generateAdapter = true)
data class M202Config(
val arc_pw: String? = null,
val role: String? = "M202",
)

@JsonClass(generateAdapter = true)
data class EmojisConfig(
val accept: String = "\uD83D\uDC4D",
Expand Down Expand Up @@ -76,6 +82,7 @@ data class RootConfig(
val emojis: EmojisConfig = EmojisConfig(),
val permissions: PermissionConfig = PermissionConfig(),
val emu: EmuConfig = EmuConfig(),
val m202: M202Config = M202Config(),
)

private val moshi = Moshi.Builder().build()
Expand Down
17 changes: 17 additions & 0 deletions src/main/kotlin/de/bigboot/ggtools/fang/db/M202Confirm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package de.bigboot.ggtools.fang.db

import org.jetbrains.exposed.dao.UUIDEntity
import org.jetbrains.exposed.dao.UUIDEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.UUIDTable
import java.util.*

class M202Confirm(id: EntityID<UUID>) : UUIDEntity(id) {
companion object : UUIDEntityClass<M202Confirm>(M202Confirms)

var token by M202Confirms.token
}

object M202Confirms : UUIDTable() {
val token = text("token")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@file:Suppress("ClassName", "ClassNaming", "unused", "LongMethod")

package de.bigboot.ggtools.fang.db.migrations

import org.flywaydb.core.api.migration.BaseJavaMigration
import org.flywaydb.core.api.migration.Context

class V13__add_m202_confirm : BaseJavaMigration() {
override fun migrate(context: Context) {
context.connection.prepareStatement("""
|create table if not exists M202Confirms
|(
| id binary(16) not null
| primary key,
| token text not null
|);
""".trimMargin()).execute()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ val serviceModule = module {
single { QueueMessageService() } bind AutostartService::class
single { StatusUpdateService() } bind AutostartService::class
single { MistforgeService() } bind AutostartService::class
single { M202VerifyServiceImpl() } bind AutostartService::class
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package de.bigboot.ggtools.fang.service

import de.bigboot.ggtools.fang.Config
import de.bigboot.ggtools.fang.db.M202Confirm
import de.bigboot.ggtools.fang.db.M202Confirms
import de.bigboot.ggtools.fang.utils.*
import discord4j.core.GatewayDiscordClient
import discord4j.core.event.domain.message.MessageCreateEvent
import discord4j.core.`object`.entity.channel.PrivateChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.withContext
import net.lingala.zip4j.io.inputstream.ZipInputStream
import net.lingala.zip4j.model.LocalFileHeader
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.ByteString.Companion.encode
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.InputStream
import java.security.MessageDigest
import javax.xml.parsers.DocumentBuilderFactory


class M202VerifyServiceImpl : AutostartService, KoinComponent {
private val client by inject<GatewayDiscordClient>()
private val http = OkHttpClient()
private val database by inject<Database>()
private val hasher = MessageDigest.getInstance("SHA-256")

init {
client.eventDispatcher.on<MessageCreateEvent>()
.onEachSafe(this::handleCommandEvent)
.launch()
}

private fun extractFileFromEncryptedZip(input: InputStream, password: String?, targetFileName: String): InputStream? {
val zip = ZipInputStream(input, password?.toCharArray())

var localFileHeader: LocalFileHeader? = zip.nextEntry
while (localFileHeader != null) {
if(localFileHeader.fileName == targetFileName) {
return zip
}
localFileHeader = zip.nextEntry
}

return null
}

private fun getUserTokens(input: InputStream, game: String): List<String> {
val doc = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(input)

val tokens = doc.getElementsByTagName("user")
return sequence {
for (i in 0 until tokens.length) {
val element = tokens.item(i)
if(element.attributes.getNamedItem("gameabbr").nodeValue == game) {
yield(element.attributes.getNamedItem("token").nodeValue)
}
}
}.toList()
}

private fun verifyToken(token: String) = transaction(database) {
val hash = hasher.digest(token.encode(Charsets.US_ASCII).toByteArray()).fold("") { str, it ->
str + "%02x".format(it)
}

if (M202Confirm.find { M202Confirms.token eq hash }.any()) {
return@transaction false
}

M202Confirm.new { this.token = hash }

return@transaction true
}

private suspend fun handleCommandEvent(event: MessageCreateEvent) {
val msg = event.message
val channel = event.message.channel.awaitSingle();

if (channel !is PrivateChannel) {
return
}

val arcZone = msg.attachments.firstOrNull { it.filename == "ArcZone.dat" } ?: return

if (arcZone.size > 2 * 1024 * 1024) {
channel.createMessageCompat {
content("It looks like you tried to verify your access to M202, but the file you sent exceeds the size limit, if you believe this is an error please contact one of the moderators.")
}.awaitSafe()
return
}

val response = http.newCall(Request.Builder().url(arcZone.url).build()).await()
val body = response.body() ?: return

val tokens = withContext(Dispatchers.IO) {
extractFileFromEncryptedZip(body.byteStream(), Config.m202.arc_pw, "ArcZone.xml")?.let {
getUserTokens(it, "m202")
}
} ?: return

val valid = tokens.any { verifyToken(it) }

if(!valid) {
channel.createMessageCompat {
content("This token has already been used to verify access and cannot be used again.")
}.awaitSafe()
return
}

for (guild in client.guilds.awaitSafe())
{
val member = guild.getMemberById(msg.author.get().id).awaitSafe() ?: continue
val role = guild.roles.filter { it.name == Config.m202.role }.awaitFirstOrNull() ?: continue
member.addRole(role.id, "Assigned by fang using token verification").awaitSafe()

channel.createMessageCompat {
content("Role ${role.name} has been assigned in ${guild.name}.")
}.awaitSafe()
}
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/de/bigboot/ggtools/fang/utils/OkHttpExt.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.bigboot.ggtools.fang.utils

import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

class OkHttpExt {

}suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response)
}

override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
})
}
}

0 comments on commit 5d69661

Please sign in to comment.