From 215a4670db5ee9b1a1c46dc06470a11048827475 Mon Sep 17 00:00:00 2001 From: LamGC Date: Sat, 18 Jun 2022 09:20:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(metrics):=20=E8=BF=90=E8=A1=8C=E6=8C=87?= =?UTF-8?q?=E6=A0=87=E6=9C=8D=E5=8A=A1=E7=AB=AF=E6=94=AF=E6=8C=81=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=20HTTP=20=E8=AE=A4=E8=AF=81.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持对运行指标服务端设置 HTTP 认证, 以防止运行指标被非法获取. --- scalabot-app/src/main/kotlin/AppConfigs.kt | 9 +- scalabot-app/src/main/kotlin/AppMain.kt | 6 +- .../main/kotlin/util/UsernameAuthenticator.kt | 65 ++++++++++ .../kotlin/util/UsernameAuthenticatorTest.kt | 114 ++++++++++++++++++ 4 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 scalabot-app/src/main/kotlin/util/UsernameAuthenticator.kt create mode 100644 scalabot-app/src/test/kotlin/util/UsernameAuthenticatorTest.kt diff --git a/scalabot-app/src/main/kotlin/AppConfigs.kt b/scalabot-app/src/main/kotlin/AppConfigs.kt index f67e43a..09b38c6 100644 --- a/scalabot-app/src/main/kotlin/AppConfigs.kt +++ b/scalabot-app/src/main/kotlin/AppConfigs.kt @@ -5,10 +5,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import mu.KotlinLogging -import net.lamgc.scalabot.util.ArtifactSerializer -import net.lamgc.scalabot.util.AuthenticationSerializer -import net.lamgc.scalabot.util.MavenRepositoryConfigSerializer -import net.lamgc.scalabot.util.ProxyTypeSerializer +import net.lamgc.scalabot.util.* import org.eclipse.aether.artifact.Artifact import org.eclipse.aether.repository.Authentication import org.eclipse.aether.repository.Proxy @@ -92,7 +89,8 @@ internal data class ProxyConfig( internal data class MetricsConfig( val enable: Boolean = false, val port: Int = 9386, - val bindAddress: String? = "0.0.0.0" + val bindAddress: String? = "0.0.0.0", + val authenticator: UsernameAuthenticator? = null ) /** @@ -322,6 +320,7 @@ private object GsonConst { .registerTypeAdapter(DefaultBotOptions.ProxyType::class.java, ProxyTypeSerializer) .registerTypeAdapter(MavenRepositoryConfig::class.java, MavenRepositoryConfigSerializer) .registerTypeAdapter(Authentication::class.java, AuthenticationSerializer) + .registerTypeAdapter(UsernameAuthenticator::class.java, UsernameAuthenticatorSerializer) .create() val botConfigGson: Gson = baseGson.newBuilder() diff --git a/scalabot-app/src/main/kotlin/AppMain.kt b/scalabot-app/src/main/kotlin/AppMain.kt index cb2027a..6b1b623 100644 --- a/scalabot-app/src/main/kotlin/AppMain.kt +++ b/scalabot-app/src/main/kotlin/AppMain.kt @@ -39,14 +39,15 @@ fun main(args: Array): Unit = runBlocking { * 启动运行指标服务器. * 使用 Prometheus 指标格式. */ -internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics) { +internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics): HTTPServer? { if (!config.enable) { log.debug { "运行指标服务器已禁用." } - return + return null } val builder = HTTPServer.Builder() .withDaemonThreads(true) + .withAuthenticator(config.authenticator) .withPort(config.port) .withHostname(config.bindAddress) @@ -54,6 +55,7 @@ internal fun startMetricsServer(config: MetricsConfig = Const.config.metrics) { .build() .registerShutdownHook() log.info { "运行指标服务器已启动. (Port: ${httpServer.port})" } + return httpServer } internal class Launcher(private val config: AppConfig = Const.config) : AutoCloseable { diff --git a/scalabot-app/src/main/kotlin/util/UsernameAuthenticator.kt b/scalabot-app/src/main/kotlin/util/UsernameAuthenticator.kt new file mode 100644 index 0000000..b5b8770 --- /dev/null +++ b/scalabot-app/src/main/kotlin/util/UsernameAuthenticator.kt @@ -0,0 +1,65 @@ +package net.lamgc.scalabot.util + +import com.google.gson.* +import com.sun.net.httpserver.BasicAuthenticator +import java.lang.reflect.Type + +class UsernameAuthenticator(private val username: String, private val password: String) : + BasicAuthenticator("metrics") { + override fun checkCredentials(username: String?, password: String?): Boolean = + this.username == username && this.password == password + + fun toJsonObject(): JsonObject = JsonObject().apply { + addProperty("username", username) + addProperty("password", password) + } + + override fun equals(other: Any?): Boolean { + return other is UsernameAuthenticator && this.username == other.username && this.password == other.password + } + + override fun hashCode(): Int { + var result = username.hashCode() + result = 31 * result + password.hashCode() + return result + } + +} + +object UsernameAuthenticatorSerializer : JsonSerializer, + JsonDeserializer { + + override fun serialize( + src: UsernameAuthenticator, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + return src.toJsonObject() + } + + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): UsernameAuthenticator? { + if (json.isJsonNull) { + return null + } else if (!json.isJsonObject) { + throw JsonParseException("Invalid attribute value type.") + } + + val jsonObj = json.asJsonObject + + if (jsonObj["username"]?.isJsonPrimitive != true) { + throw JsonParseException("Invalid attribute value: username") + } else if (jsonObj["password"]?.isJsonPrimitive != true) { + throw JsonParseException("Invalid attribute value: password") + } + + if (jsonObj["username"].asString.isEmpty() || jsonObj["password"].asString.isEmpty()) { + throw JsonParseException("`username` or `password` is empty.") + } + return UsernameAuthenticator(jsonObj["username"].asString, jsonObj["password"].asString) + } + +} diff --git a/scalabot-app/src/test/kotlin/util/UsernameAuthenticatorTest.kt b/scalabot-app/src/test/kotlin/util/UsernameAuthenticatorTest.kt new file mode 100644 index 0000000..92629db --- /dev/null +++ b/scalabot-app/src/test/kotlin/util/UsernameAuthenticatorTest.kt @@ -0,0 +1,114 @@ +package net.lamgc.scalabot.util + +import com.google.gson.* +import org.junit.jupiter.api.assertThrows +import kotlin.test.* + +class UsernameAuthenticatorTest { + + @Test + fun checkCredentialsTest() { + val authenticator = UsernameAuthenticator("testUser", "testPassword") + assertTrue(authenticator.checkCredentials("testUser", "testPassword")) + assertFalse(authenticator.checkCredentials("falseUser", "testPassword")) + assertFalse(authenticator.checkCredentials("testUser", "falsePassword")) + } + + @Test + fun toJsonObjectTest() { + val authenticator = UsernameAuthenticator("testUser", "testPassword") + val jsonObject = authenticator.toJsonObject() + assertEquals("testUser", jsonObject["username"]?.asString) + assertEquals("testPassword", jsonObject["password"]?.asString) + } + + @Test + fun equalsTest() { + val authenticator = UsernameAuthenticator("testUser", "testPassword") + assertEquals(authenticator, UsernameAuthenticator("testUser", "testPassword")) + assertEquals(authenticator.hashCode(), UsernameAuthenticator("testUser", "testPassword").hashCode()) + assertNotEquals(authenticator, UsernameAuthenticator("testUser", "falsePassword")) + assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("testUser", "falsePassword").hashCode()) + assertNotEquals(authenticator, UsernameAuthenticator("falseUser", "testPassword")) + assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("falseUser", "testPassword").hashCode()) + assertNotEquals(authenticator, UsernameAuthenticator("falseUser", "falsePassword")) + assertNotEquals(authenticator.hashCode(), UsernameAuthenticator("falseUser", "falsePassword").hashCode()) + assertFalse(authenticator.equals(null)) + } + +} + +class UsernameAuthenticatorSerializerTest { + + @Test + fun serializeTest() { + val authenticator = UsernameAuthenticator("testUser", "testPassword") + val jsonElement = UsernameAuthenticatorSerializer.serialize(authenticator, null, null) + assertTrue(jsonElement.isJsonObject) + val jsonObject = jsonElement.asJsonObject + assertEquals("testUser", jsonObject["username"]?.asString) + assertEquals("testPassword", jsonObject["password"]?.asString) + } + + @Test + fun deserializeTest() { + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonArray(), null, null) + } + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonPrimitive(""), null, null) + } + assertNull(UsernameAuthenticatorSerializer.deserialize(JsonNull.INSTANCE, null, null)) + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonObject().apply { + addProperty("username", "testUser") + }, null, null) + } + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonObject().apply { + addProperty("username", "testUser") + add("password", JsonArray()) + }, null, null) + } + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonObject().apply { + addProperty("password", "testPassword") + }, null, null) + } + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonObject().apply { + add("username", JsonArray()) + addProperty("password", "testPassword") + }, null, null) + } + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonObject().apply { + addProperty("username", "") + addProperty("password", "") + }, null, null) + } + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonObject().apply { + addProperty("username", "testUser") + addProperty("password", "") + }, null, null) + } + assertThrows { + UsernameAuthenticatorSerializer.deserialize(JsonObject().apply { + addProperty("username", "") + addProperty("password", "testPassword") + }, null, null) + } + + val authenticator = UsernameAuthenticatorSerializer.deserialize(JsonObject().apply { + addProperty("username", "testUser") + addProperty("password", "testPassword") + }, null, null) + assertNotNull(authenticator) + + assertTrue(authenticator.checkCredentials("testUser", "testPassword")) + assertFalse(authenticator.checkCredentials("falseUser", "testPassword")) + assertFalse(authenticator.checkCredentials("testUser", "falsePassword")) + } + +}