implementation(files("libs/nsapi4j.jar"))
+ implementation("de.mkammerer:argon2-jvm:2.11")
+
implementation("org.apache.groovy:groovy-jsr223:4.0.22")
implementation("io.ktor:ktor-client-core:3.0.2")
package info.mechyrdia
+import info.mechyrdia.auth.Argon2Hasher
import info.mechyrdia.data.Id
import info.mechyrdia.data.NationData
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PolymorphicKind
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonPrimitive
import java.io.File
+import java.nio.charset.Charset
@Serializable
sealed class FileStorageConfig {
data object GridFs : FileStorageConfig()
}
+@Serializable
+sealed class StoredPassword {
+ abstract fun verify(attempt: String): Boolean
+
+ open fun toCanonical(): StoredPassword? = null
+
+ @Serializable
+ @SerialName("plaintext")
+ data class InPlaintext(val password: String) : StoredPassword() {
+ override fun verify(attempt: String): Boolean {
+ return password == attempt
+ }
+
+ override fun toCanonical(): StoredPassword = HashedArgon2(Argon2Hasher.createHash(password))
+ }
+
+ @Serializable
+ @SerialName("argon2")
+ data class HashedArgon2(val hash: String) : StoredPassword() {
+ override fun verify(attempt: String): Boolean {
+ return Argon2Hasher.verifyHash(hash, attempt)
+ }
+ }
+}
+
@Serializable
data class OpenAiConfig(
val token: String,
val isDevMode: Boolean = false,
- val storage: FileStorageConfig = FileStorageConfig.Flat(".."),
+ val storage: FileStorageConfig = FileStorageConfig.GridFs,
val dbName: String = "nslore",
val dbConn: String = "mongodb://localhost:27017",
val ownerNation: String = "mechyrdia",
- val emergencyPassword: String? = null,
+ @Serializable(with = StoredPasswordConfigJsonSerializer::class)
+ val emergencyPassword: StoredPassword? = null,
val openAi: OpenAiConfig? = null,
) {
+ fun toCanonical() = emergencyPassword?.toCanonical()?.let { canonicalEmergencyPassword ->
+ copy(emergencyPassword = canonicalEmergencyPassword)
+ }
+
companion object {
val Current: Configuration by lazy {
val file = File(System.getProperty("info.mechyrdia.configpath", "./config.json"))
if (file.exists())
file.deleteRecursively()
- file.writeText(JsonFileCodec.encodeToString(serializer(), Configuration()), Charsets.UTF_8)
+ file.writeText(JsonFileCodec.encodeToString(serializer(), Configuration()), Utf8)
}
- JsonFileCodec.decodeFromString(serializer(), file.readText(Charsets.UTF_8))
+ val config = JsonFileCodec.decodeFromString(serializer(), file.readText(Utf8))
+
+ config.toCanonical()?.also { newConfig ->
+ file.writeText(JsonFileCodec.encodeToString(serializer(), newConfig), Utf8)
+ } ?: config
}
}
}
get() = Id(Configuration.Current.ownerNation)
const val MainDomainName = "https://mechyrdia.info"
+
+inline val Utf8: Charset
+ get() = Charsets.UTF_8
+
+object StoredPasswordConfigJsonSerializer : KSerializer<StoredPassword> {
+ private val defaultSerializer = StoredPassword.serializer()
+
+ @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class)
+ override val descriptor: SerialDescriptor
+ get() = buildSerialDescriptor("StoredPasswordConfigJsonSerializer", PolymorphicKind.SEALED)
+
+ override fun serialize(encoder: Encoder, value: StoredPassword) {
+ defaultSerializer.serialize(encoder, value)
+ }
+
+ override fun deserialize(decoder: Decoder): StoredPassword {
+ return if (decoder is JsonDecoder) {
+ val element = decoder.decodeJsonElement()
+ if (element is JsonPrimitive && element.isString)
+ StoredPassword.InPlaintext(element.content)
+ else
+ decoder.json.decodeFromJsonElement(defaultSerializer, element)
+ } else
+ defaultSerializer.deserialize(decoder)
+ }
+}
--- /dev/null
+package info.mechyrdia.auth
+
+import de.mkammerer.argon2.Argon2
+import de.mkammerer.argon2.Argon2Advanced
+import de.mkammerer.argon2.Argon2Factory
+import info.mechyrdia.Utf8
+
+private val argon2Instance = Argon2Factory.createAdvanced()
+
+object Argon2Hasher {
+ const val ITERATIONS = 3
+ const val MEMORY = 22
+ const val PARALLELISM = 1
+
+ fun createHash(plaintext: String): String {
+ return Instance.hash(ITERATIONS, MEMORY, PARALLELISM, plaintext.toByteArray(Utf8))
+ }
+
+ fun verifyHash(hash: String, attempt: String): Boolean {
+ return Instance.verify(hash, attempt.toByteArray(Utf8))
+ }
+
+ object Instance : Argon2 by argon2Instance
+
+ object Advanced : Argon2Advanced by argon2Instance
+}
import info.mechyrdia.route.installCsrfToken
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
-import io.ktor.http.content.PartData
import io.ktor.http.defaultForFileExtension
import io.ktor.server.application.ApplicationCall
import io.ktor.server.html.respondHtml
import io.ktor.server.plugins.MissingRequestParameterException
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
-import io.ktor.utils.io.toByteArray
import kotlinx.html.*
fun Map<String, StoredFileType>.sortedAsFiles() = toList()
package info.mechyrdia.data
+import info.mechyrdia.Utf8
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.withCharsetIfNeeded
fun <T, C : XmlTagConsumer<T>> C.root(name: String, attributes: Map<String, String> = emptyMap(), namespace: String? = null, block: (XmlTag.() -> Unit)? = null) = XmlTag(name, this, attributes, namespace, false, block == null).visitAndFinalize(this, block ?: emptyBlock)
suspend fun ApplicationCall.respondXml(status: HttpStatusCode? = null, contentType: ContentType = ContentType.Text.Xml, prettyPrint: Boolean = true, block: XmlTagConsumer<String>.() -> String) {
- respondText(createXml(prettyPrint).block(), contentType.withCharsetIfNeeded(Charsets.UTF_8), status)
+ respondText(createXml(prettyPrint).block(), contentType.withCharsetIfNeeded(Utf8), status)
}
package info.mechyrdia.lore
import info.mechyrdia.JsonFileCodec
+import info.mechyrdia.Utf8
import info.mechyrdia.concatenated
import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
suspend fun loadFontsJson(): FontAssetsManifest {
val fontsFile = FileStorage.instance.readFile(fontsJsonPath) ?: return FontAssetsManifest(emptyMap())
- val fontsJson = String(fontsFile)
+ val fontsJson = String(fontsFile, Utf8)
val fontsMap = JsonFileCodec.decodeFromString(MapSerializer(String.serializer(), FontAssetInfo.serializer()), fontsJson)
return FontAssetsManifest(fontsMap)
}
package info.mechyrdia.lore
import info.mechyrdia.JsonStorageCodec
+import info.mechyrdia.Utf8
import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
import io.ktor.util.hex
suspend fun loadTemplate(name: String): ParserTree {
val templateFile = StoragePath.templateDir / "$name.tpl"
val template = FileStorage.instance.readFile(templateFile) ?: return emptyList()
- return ParserState.parseText(String(template))
+ return ParserState.parseText(String(template, Utf8))
}
suspend fun runTemplateWith(name: String, args: Map<String, ParserTree>): ParserTree {
cache.getOrPut(digest) {
scriptEngineSync.withLock {
- (scriptEngine as Compilable).compile(String(script))
+ (scriptEngine as Compilable).compile(String(script, Utf8))
}
}
}
package info.mechyrdia.lore
import info.mechyrdia.JsonStorageCodec
+import info.mechyrdia.Utf8
import info.mechyrdia.concat
import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
private suspend fun loadJsonData(lorePath: List<String>): JsonObject {
val jsonPath = lorePath.dropLast(1) + listOf("${lorePath.last()}.json")
val bytes = FileStorage.instance.readFile(StoragePath.jsonDocDir / jsonPath) ?: return JsonObject(emptyMap())
- return JsonStorageCodec.parseToJsonElement(String(bytes)) as JsonObject
+ return JsonStorageCodec.parseToJsonElement(String(bytes, Utf8)) as JsonObject
}
suspend fun loadFactbookContext(lorePath: List<String>): Map<String, ParserTree> {
suspend fun loadFactbook(lorePath: List<String>): ParserTree? {
val filePath = StoragePath.articleDir / lorePath
val bytes = FileStorage.instance.readFile(filePath) ?: return null
- val inputTree = ParserState.parseText(String(bytes))
+ val inputTree = ParserState.parseText(String(bytes, Utf8))
return inputTree.preProcess(PreProcessorContext(loadFactbookContext(lorePath) + PreProcessorContext.defaults(lorePath)))
}
}
import info.mechyrdia.JsonFileCodec
import info.mechyrdia.OwnerNationId
+import info.mechyrdia.Utf8
import info.mechyrdia.auth.createCsrfToken
import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
suspend fun loadExternalLinks(): List<NavItem> {
val extraLinksFile = FileStorage.instance.readFile(extraLinksPath) ?: return emptyList()
- val extraLinksJson = String(extraLinksFile)
+ val extraLinksJson = String(extraLinksFile, Utf8)
val extraLinks = JsonFileCodec.decodeFromString(ListSerializer(ExternalLink.serializer()), extraLinksJson)
return if (extraLinks.isEmpty())
emptyList()
import info.mechyrdia.JsonFileCodec
import info.mechyrdia.MainDomainName
+import info.mechyrdia.Utf8
import info.mechyrdia.data.FileStorage
import info.mechyrdia.data.StoragePath
import info.mechyrdia.data.XmlTagConsumer
private val quotesListGetter by storedData(StoragePath("quotes.json")) { jsonPath ->
FileStorage.instance.readFile(jsonPath)?.let {
- JsonFileCodec.decodeFromString(ListSerializer(Quote.serializer()), String(it))
+ JsonFileCodec.decodeFromString(ListSerializer(Quote.serializer()), String(it, Utf8))
}
}
package info.mechyrdia.robot
+import info.mechyrdia.Utf8
import io.ktor.client.request.forms.FormBuilder
import io.ktor.http.ContentType
import io.ktor.http.Headers
) : Tokenizable {
override fun getTexts(): List<String> {
return if (contentType.match(ContentType.Text.Any))
- listOf(String(content))
+ listOf(String(content, Utf8))
else emptyList()
}
}
import info.mechyrdia.Configuration
import info.mechyrdia.MainDomainName
import info.mechyrdia.OpenAiConfig
+import info.mechyrdia.Utf8
import info.mechyrdia.concat
import info.mechyrdia.data.DataDocument
import info.mechyrdia.data.DocumentTable
val newId = robotClient.uploadFile(
"assistants",
FileUpload(
- text.toByteArray(),
- ContentType.Text.Plain.withCharset(Charsets.UTF_8),
+ text.toByteArray(Utf8),
+ ContentType.Text.Plain.withCharset(Utf8),
name.toOpenAiName()
)
).id
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
-import io.ktor.http.content.PartData
import io.ktor.resources.Resource
import io.ktor.server.application.ApplicationCall
import io.ktor.server.html.respondHtml
package info.mechyrdia.route
+import info.mechyrdia.Utf8
import info.mechyrdia.auth.WebDavToken
import info.mechyrdia.auth.toNationId
import info.mechyrdia.concat
val auth = authorization() ?: return null
if (!auth.startsWith("Basic ")) return null
val basic = auth.substring(6)
- return String(base64Decoder.decode(basic))
+ return String(base64Decoder.decode(basic), Utf8)
.split(':', limit = 2)
.let { (user, pass) -> user to pass }
}