From 45d329f692dd7373a4dbf5f85fab07acb0812219 Mon Sep 17 00:00:00 2001 From: carbotaniuman Date: Fri, 19 Jun 2020 20:16:24 +0000 Subject: [PATCH] Switch from Gson to Jackson for better null checks and invalid property support --- CHANGELOG.md | 1 + build.gradle | 6 +- src/main/java/mdnet/base/Main.java | 104 --------------- src/main/java/mdnet/base/MangaDexClient.java | 10 +- src/main/java/mdnet/base/ServerHandler.java | 84 ------------- src/main/java/mdnet/base/ServerSettings.java | 118 ------------------ src/main/kotlin/mdnet/base/Constants.kt | 5 +- src/main/kotlin/mdnet/base/Main.kt | 103 +++++++++++++++ src/main/kotlin/mdnet/base/ServerHandler.kt | 93 ++++++++++++++ src/main/kotlin/mdnet/base/Statistics.kt | 16 +-- .../{Netty.kt => netty/ApplicationNetty.kt} | 6 +- .../kotlin/mdnet/base/{ => netty}/Keys.kt | 20 ++- .../mdnet/base/{ => netty}/WebUiNetty.kt | 2 +- .../kotlin/mdnet/base/server/Application.kt | 8 +- .../kotlin/mdnet/base/server/ImageServer.kt | 4 +- src/main/kotlin/mdnet/base/server/WebUi.kt | 4 +- src/main/kotlin/mdnet/base/server/common.kt | 9 +- .../mdnet/base/settings/ClientSettings.kt | 25 ++-- .../mdnet/base/settings/ServerSettings.kt | 21 ++++ 19 files changed, 283 insertions(+), 356 deletions(-) delete mode 100644 src/main/java/mdnet/base/Main.java delete mode 100644 src/main/java/mdnet/base/ServerHandler.java delete mode 100644 src/main/java/mdnet/base/ServerSettings.java create mode 100644 src/main/kotlin/mdnet/base/Main.kt create mode 100644 src/main/kotlin/mdnet/base/ServerHandler.kt rename src/main/kotlin/mdnet/base/{Netty.kt => netty/ApplicationNetty.kt} (96%) rename src/main/kotlin/mdnet/base/{ => netty}/Keys.kt (94%) rename src/main/kotlin/mdnet/base/{ => netty}/WebUiNetty.kt (99%) mode change 100755 => 100644 create mode 100644 src/main/kotlin/mdnet/base/settings/ServerSettings.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index c083e6b..181da99 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- [2020-06-19] Errored out on invalid settings.json tokens [@carbotaniuman] ### Changed - [2020-06-19] Changed default CPU thread count to `4` by [@lflare]. diff --git a/build.gradle b/build.gradle index ee28cdb..59a7e72 100644 --- a/build.gradle +++ b/build.gradle @@ -20,16 +20,16 @@ dependencies { compileOnly group:"dev.afanasev", name: "sekret-annotation", version: "0.0.3" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "org.jetbrains.kotlin:kotlin-reflect" + implementation group: "commons-io", name: "commons-io", version: "2.7" - implementation group: "com.konghq", name: "unirest-java", version: "3.7.02" implementation group: "org.http4k", name: "http4k-core", version: "$http_4k_version" - implementation group: "org.http4k", name: "http4k-format-gson", version: "$http_4k_version" + implementation group: "org.http4k", name: "http4k-format-jackson", version: "$http_4k_version" implementation group: "org.http4k", name: "http4k-client-apache", version: "$http_4k_version" implementation group: "org.http4k", name: "http4k-server-netty", version: "$http_4k_version" runtimeOnly group:"io.netty", name: "netty-tcnative-boringssl-static", version: "2.0.30.Final" - implementation group:"ch.qos.logback", name: "logback-classic", version: "1.2.1" implementation group: "org.jetbrains.exposed", name: "exposed-core", version: "$exposed_version" diff --git a/src/main/java/mdnet/base/Main.java b/src/main/java/mdnet/base/Main.java deleted file mode 100644 index 1945d54..0000000 --- a/src/main/java/mdnet/base/Main.java +++ /dev/null @@ -1,104 +0,0 @@ -package mdnet.base; - -import mdnet.base.settings.ClientSettings; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.regex.Pattern; - -import static mdnet.base.Constants.GSON; - -public class Main { - private final static Logger LOGGER = LoggerFactory.getLogger(Main.class); - - public static void main(String[] args) { - System.out.println("Mangadex@Home Client " + Constants.CLIENT_VERSION + " (Build " + Constants.CLIENT_BUILD - + ") initializing\n"); - System.out.println("Copyright (c) 2020, MangaDex Network"); - - String file = "settings.json"; - if (args.length == 1) { - file = args[0]; - } else if (args.length != 0) { - dieWithError("Expected one argument: path to config file, or nothing"); - } - - ClientSettings settings; - - try { - settings = GSON.fromJson(new FileReader(file), ClientSettings.class); - } catch (FileNotFoundException ignored) { - settings = new ClientSettings(); - LOGGER.warn("Settings file {} not found, generating file", file); - try (FileWriter writer = new FileWriter(file)) { - writer.write(GSON.toJson(settings)); - } catch (IOException e) { - dieWithError(e); - } - } - - validateSettings(settings); - - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Client settings loaded: {}", settings); - } - - MangaDexClient client = new MangaDexClient(settings); - Runtime.getRuntime().addShutdownHook(new Thread(client::shutdown)); - client.runLoop(); - } - - public static void dieWithError(Throwable e) { - if (LOGGER.isErrorEnabled()) { - LOGGER.error("Critical Error", e); - } - System.exit(1); - } - - public static void dieWithError(String error) { - if (LOGGER.isErrorEnabled()) { - LOGGER.error("Critical Error: {}", error); - } - System.exit(1); - } - - public static void validateSettings(ClientSettings settings) { - if (!isSecretValid(settings.getClientSecret())) - dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters"); - - if (settings.getClientPort() == 0) { - dieWithError("Config Error: Invalid port number"); - } - - if (settings.getMaxCacheSizeInMebibytes() < 1024) { - dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)"); - } - - if (settings.getThreads() < 4) { - dieWithError("Config Error: Invalid number of threads, must be >= 4"); - } - - if (settings.getMaxMebibytesPerHour() < 0) { - dieWithError("Config Error: Max bandwidth must be >= 0"); - } - - if (settings.getMaxKilobitsPerSecond() < 0) { - dieWithError("Config Error: Max burst rate must be >= 0"); - } - - if (settings.getWebSettings() != null) { - if (settings.getWebSettings().getUiPort() == 0) { - dieWithError("Config Error: Invalid UI port number"); - } - } - } - - public static boolean isSecretValid(String clientSecret) { - final int CLIENT_KEY_LENGTH = 52; - return Pattern.matches("^[a-zA-Z0-9]{" + CLIENT_KEY_LENGTH + "}$", clientSecret); - } -} diff --git a/src/main/java/mdnet/base/MangaDexClient.java b/src/main/java/mdnet/base/MangaDexClient.java index 8b16b12..ae276f1 100644 --- a/src/main/java/mdnet/base/MangaDexClient.java +++ b/src/main/java/mdnet/base/MangaDexClient.java @@ -3,6 +3,7 @@ package mdnet.base; import mdnet.base.settings.ClientSettings; import mdnet.base.server.ApplicationKt; import mdnet.base.server.WebUiKt; +import mdnet.base.settings.ServerSettings; import mdnet.cache.DiskLruCache; import org.http4k.server.Http4kServer; import org.slf4j.Logger; @@ -19,7 +20,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import static mdnet.base.Constants.GSON; +import static mdnet.base.Constants.JACKSON; public class MangaDexClient { private final static Logger LOGGER = LoggerFactory.getLogger(MangaDexClient.class); @@ -62,8 +63,8 @@ public class MangaDexClient { DiskLruCache.Snapshot snapshot = cache.get("statistics"); if (snapshot != null) { - String json = snapshot.getString(0); - statistics.set(GSON.fromJson(json, Statistics.class)); + statistics.set(JACKSON.readValue(snapshot.getInputStream(0), Statistics.class)); + snapshot.close(); } else { statistics.set(new Statistics()); } @@ -215,8 +216,7 @@ public class MangaDexClient { DiskLruCache.Editor editor = cache.edit("statistics"); if (editor != null) { - String json = GSON.toJson(statistics.get(), Statistics.class); - editor.setString(0, json); + JACKSON.writeValue(editor.newOutputStream(0), Statistics.class); editor.commit(); } } diff --git a/src/main/java/mdnet/base/ServerHandler.java b/src/main/java/mdnet/base/ServerHandler.java deleted file mode 100644 index 13a4b2e..0000000 --- a/src/main/java/mdnet/base/ServerHandler.java +++ /dev/null @@ -1,84 +0,0 @@ -package mdnet.base; - -import kong.unirest.HttpResponse; -import kong.unirest.Unirest; -import kong.unirest.json.JSONObject; -import mdnet.base.settings.ClientSettings; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; - -public class ServerHandler { - private final static Logger LOGGER = LoggerFactory.getLogger(ServerHandler.class); - private static final String SERVER_ADDRESS = "https://api.mangadex.network/"; - - private final ClientSettings settings; - - public ServerHandler(ClientSettings settings) { - this.settings = settings; - } - - public boolean logoutFromControl() { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Disconnecting from the control server"); - } - - HashMap params = new HashMap<>(); - params.put("secret", settings.getClientSecret()); - - HttpResponse json = Unirest.post(SERVER_ADDRESS + "stop").header("Content-Type", "application/json") - .body(new JSONObject(params)).asEmpty(); - - return json.isSuccess(); - } - - public ServerSettings loginToControl() { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Connecting to the control server"); - } - - HashMap params = new HashMap<>(); - params.put("secret", settings.getClientSecret()); - params.put("port", settings.getClientPort()); - params.put("disk_space", settings.getMaxCacheSizeInMebibytes() * 1024 * 1024 /* MiB to bytes */); - params.put("network_speed", settings.getMaxKilobitsPerSecond() * 1000 / 8 /* Kbps to bytes */); - params.put("build_version", Constants.CLIENT_BUILD); - - HttpResponse response = Unirest.post(SERVER_ADDRESS + "ping") - .header("Content-Type", "application/json").body(new JSONObject(params)).asObject(ServerSettings.class); - - if (response.isSuccess()) { - return response.getBody(); - } else { - // unirest deserializes errors into an object with all null fields instead of a - // null object - return null; - } - } - - public ServerSettings pingControl(ServerSettings old) { - if (LOGGER.isInfoEnabled()) { - LOGGER.info("Pinging the control server"); - } - - HashMap params = new HashMap<>(); - params.put("secret", settings.getClientSecret()); - params.put("port", settings.getClientPort()); - params.put("disk_space", settings.getMaxCacheSizeInMebibytes() * 1024 * 1024 /* MiB to bytes */); - params.put("network_speed", settings.getMaxKilobitsPerSecond() * 1000 / 8 /* Kbps to bytes */); - params.put("build_version", Constants.CLIENT_BUILD); - params.put("tls_created_at", old.getTls().getCreatedAt()); - - HttpResponse response = Unirest.post(SERVER_ADDRESS + "ping") - .header("Content-Type", "application/json").body(new JSONObject(params)).asObject(ServerSettings.class); - - if (response.isSuccess()) { - return response.getBody(); - } else { - // unirest deserializes errors into an object with all null fields instead of a - // null object - return null; - } - } -} diff --git a/src/main/java/mdnet/base/ServerSettings.java b/src/main/java/mdnet/base/ServerSettings.java deleted file mode 100644 index 00a292a..0000000 --- a/src/main/java/mdnet/base/ServerSettings.java +++ /dev/null @@ -1,118 +0,0 @@ -package mdnet.base; - -import com.google.gson.annotations.SerializedName; - -import java.util.Objects; - -public final class ServerSettings { - @SerializedName("image_server") - private final String imageServer; - private final TlsCert tls; - @SerializedName("latest_build") - private final int latestBuild; - private final String url; - private final boolean compromised; - - public ServerSettings(String imageServer, TlsCert tls, int latestBuild, String url, boolean compromised) { - this.imageServer = Objects.requireNonNull(imageServer); - this.tls = tls; - this.latestBuild = latestBuild; - this.url = url; - this.compromised = compromised; - } - - public String getImageServer() { - return imageServer; - } - - public TlsCert getTls() { - return tls; - } - - public int getLatestBuild() { - return latestBuild; - } - - @Override - public String toString() { - return "ServerSettings{" + "imageServer='" + imageServer + '\'' + ", tls=" + tls + ", latestBuild=" - + latestBuild + ", url='" + url + "', compromised=" + compromised + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - ServerSettings that = (ServerSettings) o; - - if (!imageServer.equals(that.imageServer)) - return false; - return Objects.equals(tls, that.tls); - } - - @Override - public int hashCode() { - int result = imageServer.hashCode(); - result = 31 * result + (tls != null ? tls.hashCode() : 0); - return result; - } - - public static final class TlsCert { - @SerializedName("created_at") - private final String createdAt; - @SerializedName("private_key") - private final String privateKey; - private final String certificate; - - public TlsCert(String createdAt, String privateKey, String certificate) { - this.createdAt = Objects.requireNonNull(createdAt); - this.privateKey = Objects.requireNonNull(privateKey); - this.certificate = Objects.requireNonNull(certificate); - } - - public String getCreatedAt() { - return createdAt; - } - - public String getPrivateKey() { - return privateKey; - } - - public String getCertificate() { - return certificate; - } - - @Override - public String toString() { - return "TlsCert{" + "createdAt='" + createdAt + '\'' + ", privateKey='" + "" + '\'' - + ", certificate='" + "" + '\'' + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - - TlsCert tlsCert = (TlsCert) o; - - if (!createdAt.equals(tlsCert.createdAt)) - return false; - if (!privateKey.equals(tlsCert.privateKey)) - return false; - return certificate.equals(tlsCert.certificate); - } - - @Override - public int hashCode() { - int result = createdAt.hashCode(); - result = 31 * result + privateKey.hashCode(); - result = 31 * result + certificate.hashCode(); - return result; - } - } -} diff --git a/src/main/kotlin/mdnet/base/Constants.kt b/src/main/kotlin/mdnet/base/Constants.kt index 6f9918f..d8d3802 100644 --- a/src/main/kotlin/mdnet/base/Constants.kt +++ b/src/main/kotlin/mdnet/base/Constants.kt @@ -1,7 +1,6 @@ package mdnet.base -import com.google.gson.Gson -import com.google.gson.GsonBuilder +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import java.time.Duration object Constants { @@ -10,5 +9,5 @@ object Constants { const val WEBUI_VERSION = "0.1.1" val MAX_AGE_CACHE: Duration = Duration.ofDays(14) @JvmField - val GSON: Gson = GsonBuilder().setPrettyPrinting().create() + val JACKSON = jacksonObjectMapper() } diff --git a/src/main/kotlin/mdnet/base/Main.kt b/src/main/kotlin/mdnet/base/Main.kt new file mode 100644 index 0000000..70573cb --- /dev/null +++ b/src/main/kotlin/mdnet/base/Main.kt @@ -0,0 +1,103 @@ +package mdnet.base + +import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.JsonMappingException +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException +import com.fasterxml.jackson.module.kotlin.readValue +import mdnet.base.Constants.JACKSON +import mdnet.base.settings.ClientSettings +import org.slf4j.LoggerFactory +import java.io.FileReader +import java.io.FileWriter +import java.io.IOException +import java.util.regex.Pattern +import kotlin.system.exitProcess + +object Main { + private val LOGGER = LoggerFactory.getLogger(Main::class.java) + + @JvmStatic + fun main(args: Array) { + println( + "Mangadex@Home Client ${Constants.CLIENT_VERSION} (Build ${Constants.CLIENT_BUILD}) initializing" + ) + println("Copyright (c) 2020, MangaDex Network") + + var file = "settings.json" + if (args.size == 1) { + file = args[0] + } else if (args.isNotEmpty()) { + dieWithError("Expected one argument: path to config file, or nothing") + } + + val settings: ClientSettings = try { + JACKSON.readValue(FileReader(file)) + } catch (e: UnrecognizedPropertyException) { + dieWithError("'${e.propertyName}' is not a valid setting") + } catch (e: JsonProcessingException) { + dieWithError(e) + } catch (ignored: IOException) { + ClientSettings().also { + LOGGER.warn("Settings file {} not found, generating file", file) + try { + FileWriter(file).use { writer -> JACKSON.writeValue(writer, it) } + } catch (e: IOException) { + dieWithError(e) + } + } + }.apply(::validateSettings) + + if (LOGGER.isInfoEnabled) { + LOGGER.info("Client settings loaded: {}", settings) + } + val client = MangaDexClient(settings) + Runtime.getRuntime().addShutdownHook(Thread { client.shutdown() }) + client.runLoop() + } + + @JvmStatic + fun dieWithError(e: Throwable): Nothing { + if (LOGGER.isErrorEnabled) { + LOGGER.error("Critical Error", e) + } + exitProcess(1) + } + + @JvmStatic + fun dieWithError(error: String): Nothing { + if (LOGGER.isErrorEnabled) { + LOGGER.error("Critical Error: {}", error) + } + exitProcess(1) + } + + private fun validateSettings(settings: ClientSettings) { + if (!isSecretValid(settings.clientSecret)) dieWithError("Config Error: API Secret is invalid, must be 52 alphanumeric characters") + if (settings.clientPort == 0) { + dieWithError("Config Error: Invalid port number") + } + if (settings.maxCacheSizeInMebibytes < 1024) { + dieWithError("Config Error: Invalid max cache size, must be >= 1024 MiB (1GiB)") + } + if (settings.threads < 4) { + dieWithError("Config Error: Invalid number of threads, must be >= 4") + } + if (settings.maxMebibytesPerHour < 0) { + dieWithError("Config Error: Max bandwidth must be >= 0") + } + if (settings.maxKilobitsPerSecond < 0) { + dieWithError("Config Error: Max burst rate must be >= 0") + } + if (settings.webSettings != null) { + if (settings.webSettings.uiPort == 0) { + dieWithError("Config Error: Invalid UI port number") + } + } + } + + private const val CLIENT_KEY_LENGTH = 52 + private fun isSecretValid(clientSecret: String): Boolean { + return Pattern.matches("^[a-zA-Z0-9]{$CLIENT_KEY_LENGTH}$", clientSecret) + } +} diff --git a/src/main/kotlin/mdnet/base/ServerHandler.kt b/src/main/kotlin/mdnet/base/ServerHandler.kt new file mode 100644 index 0000000..45ecd47 --- /dev/null +++ b/src/main/kotlin/mdnet/base/ServerHandler.kt @@ -0,0 +1,93 @@ +package mdnet.base + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.KotlinModule +import mdnet.base.settings.ClientSettings +import mdnet.base.settings.ServerSettings +import org.http4k.client.ApacheClient +import org.http4k.core.Body +import org.http4k.core.Method +import org.http4k.core.Request +import org.http4k.format.ConfigurableJackson +import org.http4k.format.asConfigurable +import org.http4k.format.withStandardMappings +import org.slf4j.LoggerFactory + +import mdnet.base.ServerHandlerJackson.auto +object ServerHandlerJackson : ConfigurableJackson( + KotlinModule() + .asConfigurable() + .withStandardMappings() + .done() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) +) + +class ServerHandler(private val settings: ClientSettings) { + private val client = ApacheClient() + + fun logoutFromControl(): Boolean { + if (LOGGER.isInfoEnabled) { + LOGGER.info("Disconnecting from the control server") + } + val params = mapOf( + "secret" to settings.clientSecret + ) + + val request = STRING_ANY_MAP_LENS(params, Request(Method.POST, SERVER_ADDRESS + "stop")) + val response = client(request) + + return response.status.successful + } + + private fun getPingParams(tlsCreatedAt: String? = null): Map = + mapOf( + "secret" to settings.clientSecret, + "port" to settings.clientPort, + "disk_space" to settings.maxCacheSizeInMebibytes * 1024 * 1024, + "network_speed" to settings.maxKilobitsPerSecond * 1000 / 8, + "build_version" to Constants.CLIENT_BUILD + ).let { + if (tlsCreatedAt != null) { + it.plus("tls_created_at" to tlsCreatedAt) + } else { + it + } + } + + fun loginToControl(): ServerSettings? { + if (LOGGER.isInfoEnabled) { + LOGGER.info("Connecting to the control server") + } + + val request = STRING_ANY_MAP_LENS(getPingParams(), Request(Method.POST, SERVER_ADDRESS + "ping")) + val response = client(request) + + return if (response.status.successful) { + SERVER_SETTINGS_LENS(response) + } else { + null + } + } + + fun pingControl(old: ServerSettings): ServerSettings? { + if (LOGGER.isInfoEnabled) { + LOGGER.info("Pinging the control server") + } + + val request = STRING_ANY_MAP_LENS(getPingParams(old.tls!!.createdAt), Request(Method.POST, SERVER_ADDRESS + "ping")) + val response = client(request) + + return if (response.status.successful) { + SERVER_SETTINGS_LENS(response) + } else { + null + } + } + + companion object { + private val LOGGER = LoggerFactory.getLogger(ServerHandler::class.java) + private val STRING_ANY_MAP_LENS = Body.auto>().toLens() + private val SERVER_SETTINGS_LENS = Body.auto().toLens() + private const val SERVER_ADDRESS = "https://api.mangadex.network/" + } +} diff --git a/src/main/kotlin/mdnet/base/Statistics.kt b/src/main/kotlin/mdnet/base/Statistics.kt index 3d841ef..e9f502b 100644 --- a/src/main/kotlin/mdnet/base/Statistics.kt +++ b/src/main/kotlin/mdnet/base/Statistics.kt @@ -1,12 +1,14 @@ package mdnet.base -import com.google.gson.annotations.SerializedName +import com.fasterxml.jackson.databind.PropertyNamingStrategy +import com.fasterxml.jackson.databind.annotation.JsonNaming +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) data class Statistics( - @field:SerializedName("requests_served") val requestsServed: Int = 0, - @field:SerializedName("cache_hits") val cacheHits: Int = 0, - @field:SerializedName("cache_misses") val cacheMisses: Int = 0, - @field:SerializedName("browser_cached") val browserCached: Int = 0, - @field:SerializedName("bytes_sent") val bytesSent: Long = 0, - @field:SerializedName("bytes_on_disk") val bytesOnDisk: Long = 0 + val requestsServed: Int = 0, + val cacheHits: Int = 0, + val cacheMisses: Int = 0, + val browserCached: Int = 0, + val bytesSent: Long = 0, + val bytesOnDisk: Long = 0 ) diff --git a/src/main/kotlin/mdnet/base/Netty.kt b/src/main/kotlin/mdnet/base/netty/ApplicationNetty.kt similarity index 96% rename from src/main/kotlin/mdnet/base/Netty.kt rename to src/main/kotlin/mdnet/base/netty/ApplicationNetty.kt index 1c2894a..9b67f6c 100644 --- a/src/main/kotlin/mdnet/base/Netty.kt +++ b/src/main/kotlin/mdnet/base/netty/ApplicationNetty.kt @@ -1,4 +1,4 @@ -package mdnet.base +package mdnet.base.netty import io.netty.bootstrap.ServerBootstrap import io.netty.channel.ChannelFactory @@ -19,7 +19,9 @@ import io.netty.handler.ssl.SslContextBuilder import io.netty.handler.stream.ChunkedWriteHandler import io.netty.handler.traffic.GlobalTrafficShapingHandler import io.netty.handler.traffic.TrafficCounter +import mdnet.base.Statistics import mdnet.base.settings.ClientSettings +import mdnet.base.settings.TlsCert import org.http4k.core.HttpHandler import org.http4k.server.Http4kChannelHandler import org.http4k.server.Http4kServer @@ -38,7 +40,7 @@ import javax.net.ssl.SSLException private val LOGGER = LoggerFactory.getLogger("Application") -class Netty(private val tls: ServerSettings.TlsCert, private val clientSettings: ClientSettings, private val statistics: AtomicReference) : ServerConfig { +class Netty(private val tls: TlsCert, private val clientSettings: ClientSettings, private val statistics: AtomicReference) : ServerConfig { override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer { private val masterGroup = NioEventLoopGroup(clientSettings.threads) private val workerGroup = NioEventLoopGroup(clientSettings.threads) diff --git a/src/main/kotlin/mdnet/base/Keys.kt b/src/main/kotlin/mdnet/base/netty/Keys.kt similarity index 94% rename from src/main/kotlin/mdnet/base/Keys.kt rename to src/main/kotlin/mdnet/base/netty/Keys.kt index 250f4ae..9ba75e2 100644 --- a/src/main/kotlin/mdnet/base/Keys.kt +++ b/src/main/kotlin/mdnet/base/netty/Keys.kt @@ -19,7 +19,7 @@ //LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. -package mdnet.base +package mdnet.base.netty import java.io.ByteArrayOutputStream import java.security.KeyFactory @@ -35,13 +35,23 @@ private const val PKCS_8_PEM_FOOTER = "-----END PRIVATE KEY-----" fun loadKey(keyDataString: String): PrivateKey? { if (keyDataString.contains(PKCS_1_PEM_HEADER)) { // OpenSSL / PKCS#1 Base64 PEM encoded file - val fixedString = keyDataString.replace(PKCS_1_PEM_HEADER, "").replace(PKCS_1_PEM_FOOTER, "") - return readPkcs1PrivateKey(base64Decode(fixedString)) + val fixedString = keyDataString.replace(PKCS_1_PEM_HEADER, "").replace( + PKCS_1_PEM_FOOTER, "") + return readPkcs1PrivateKey( + base64Decode( + fixedString + ) + ) } if (keyDataString.contains(PKCS_8_PEM_HEADER)) { // PKCS#8 Base64 PEM encoded file - val fixedString = keyDataString.replace(PKCS_8_PEM_HEADER, "").replace(PKCS_8_PEM_FOOTER, "") - return readPkcs1PrivateKey(base64Decode(fixedString)) + val fixedString = keyDataString.replace(PKCS_8_PEM_HEADER, "").replace( + PKCS_8_PEM_FOOTER, "") + return readPkcs1PrivateKey( + base64Decode( + fixedString + ) + ) } return null diff --git a/src/main/kotlin/mdnet/base/WebUiNetty.kt b/src/main/kotlin/mdnet/base/netty/WebUiNetty.kt old mode 100755 new mode 100644 similarity index 99% rename from src/main/kotlin/mdnet/base/WebUiNetty.kt rename to src/main/kotlin/mdnet/base/netty/WebUiNetty.kt index bbb4af2..4a0916d --- a/src/main/kotlin/mdnet/base/WebUiNetty.kt +++ b/src/main/kotlin/mdnet/base/netty/WebUiNetty.kt @@ -1,4 +1,4 @@ -package mdnet.base +package mdnet.base.netty import io.netty.bootstrap.ServerBootstrap import io.netty.channel.ChannelFactory diff --git a/src/main/kotlin/mdnet/base/server/Application.kt b/src/main/kotlin/mdnet/base/server/Application.kt index a29d17f..5b17429 100644 --- a/src/main/kotlin/mdnet/base/server/Application.kt +++ b/src/main/kotlin/mdnet/base/server/Application.kt @@ -1,8 +1,8 @@ /* ktlint-disable no-wildcard-imports */ package mdnet.base.server -import mdnet.base.Netty -import mdnet.base.ServerSettings +import mdnet.base.netty.Netty +import mdnet.base.settings.ServerSettings import mdnet.base.Statistics import mdnet.base.settings.ClientSettings import mdnet.cache.DiskLruCache @@ -21,7 +21,7 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting val database = Database.connect("jdbc:sqlite:cache/data.db", "org.sqlite.JDBC") val imageServer = ImageServer(cache, statistics, serverSettings.imageServer, database, isHandled) - return Timer + return timeRequest() .then(catchAllHideDetails()) .then(ServerFilters.CatchLensFailure) .then(addCommonHeaders()) @@ -39,5 +39,5 @@ fun getServer(cache: DiskLruCache, serverSettings: ServerSettings, clientSetting ) ) ) - .asServer(Netty(serverSettings.tls, clientSettings, statistics)) + .asServer(Netty(serverSettings.tls!!, clientSettings, statistics)) } diff --git a/src/main/kotlin/mdnet/base/server/ImageServer.kt b/src/main/kotlin/mdnet/base/server/ImageServer.kt index 1e7d1f8..aeeb661 100644 --- a/src/main/kotlin/mdnet/base/server/ImageServer.kt +++ b/src/main/kotlin/mdnet/base/server/ImageServer.kt @@ -200,8 +200,8 @@ class ImageServer(private val cache: DiskLruCache, private val statistics: Atomi } editor.commit() } else { - if (LOGGER.isInfoEnabled) { - LOGGER.info("Cache download for $sanitizedUri aborted") + if (LOGGER.isWarnEnabled) { + LOGGER.warn("Cache download for $sanitizedUri aborted") } editor.abort() } diff --git a/src/main/kotlin/mdnet/base/server/WebUi.kt b/src/main/kotlin/mdnet/base/server/WebUi.kt index f20af41..6de53d5 100644 --- a/src/main/kotlin/mdnet/base/server/WebUi.kt +++ b/src/main/kotlin/mdnet/base/server/WebUi.kt @@ -4,7 +4,7 @@ package mdnet.base.server import java.time.Instant import java.util.concurrent.atomic.AtomicReference import mdnet.base.Statistics -import mdnet.base.WebUiNetty +import mdnet.base.netty.WebUiNetty import mdnet.base.settings.WebSettings import org.http4k.core.Body import org.http4k.core.Method @@ -12,7 +12,7 @@ import org.http4k.core.Response import org.http4k.core.Status import org.http4k.core.then import org.http4k.filter.ServerFilters -import org.http4k.format.Gson.auto +import org.http4k.format.Jackson.auto import org.http4k.routing.ResourceLoader import org.http4k.routing.bind import org.http4k.routing.routes diff --git a/src/main/kotlin/mdnet/base/server/common.kt b/src/main/kotlin/mdnet/base/server/common.kt index bc74af1..a236ddc 100644 --- a/src/main/kotlin/mdnet/base/server/common.kt +++ b/src/main/kotlin/mdnet/base/server/common.kt @@ -41,25 +41,24 @@ fun catchAllHideDetails(): Filter { } } -val Timer = Filter { - next: HttpHandler -> { - request: Request -> +fun timeRequest(): Filter { + return Filter { next: HttpHandler -> + { request: Request -> val start = System.currentTimeMillis() val response = next(request) val latency = System.currentTimeMillis() - start if (LOGGER.isTraceEnabled && response.header("X-Uri") != null) { // Dirty hack to get sanitizedUri from ImageServer val sanitizedUri = response.header("X-Uri") - // Log in TRACE if (LOGGER.isInfoEnabled) { LOGGER.info("Request for $sanitizedUri completed in ${latency}ms") } - // Delete response header entirely response.header("X-Uri", null) } // Set response header with processing times response.header("X-Time-Taken", latency.toString()) + } } } diff --git a/src/main/kotlin/mdnet/base/settings/ClientSettings.kt b/src/main/kotlin/mdnet/base/settings/ClientSettings.kt index 24c4e95..469a3cb 100644 --- a/src/main/kotlin/mdnet/base/settings/ClientSettings.kt +++ b/src/main/kotlin/mdnet/base/settings/ClientSettings.kt @@ -1,20 +1,23 @@ package mdnet.base.settings -import com.google.gson.annotations.SerializedName +import com.fasterxml.jackson.databind.PropertyNamingStrategy +import com.fasterxml.jackson.databind.annotation.JsonNaming import dev.afanasev.sekret.Secret +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) data class ClientSettings( - @field:SerializedName("max_cache_size_in_mebibytes") val maxCacheSizeInMebibytes: Long = 20480, - @field:SerializedName("max_mebibytes_per_hour") val maxMebibytesPerHour: Long = 0, - @field:SerializedName("max_kilobits_per_second") val maxKilobitsPerSecond: Long = 0, - @field:SerializedName("client_hostname") val clientHostname: String = "0.0.0.0", - @field:SerializedName("client_port") val clientPort: Int = 443, - @field:Secret @field:SerializedName("client_secret") val clientSecret: String = "PASTE-YOUR-SECRET-HERE", - @field:SerializedName("threads") val threads: Int = 4, - @field:SerializedName("web_settings") val webSettings: WebSettings? = null + val maxCacheSizeInMebibytes: Long = 20480, + val maxMebibytesPerHour: Long = 0, + val maxKilobitsPerSecond: Long = 0, + val clientHostname: String = "0.0.0.0", + val clientPort: Int = 443, + @field:Secret val clientSecret: String = "PASTE-YOUR-SECRET-HERE", + val threads: Int = 4, + val webSettings: WebSettings? = null ) +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) data class WebSettings( - @field:SerializedName("ui_hostname") val uiHostname: String = "127.0.0.1", - @field:SerializedName("ui_port") val uiPort: Int = 8080 + val uiHostname: String = "127.0.0.1", + val uiPort: Int = 8080 ) diff --git a/src/main/kotlin/mdnet/base/settings/ServerSettings.kt b/src/main/kotlin/mdnet/base/settings/ServerSettings.kt new file mode 100644 index 0000000..648417f --- /dev/null +++ b/src/main/kotlin/mdnet/base/settings/ServerSettings.kt @@ -0,0 +1,21 @@ +package mdnet.base.settings + +import com.fasterxml.jackson.databind.PropertyNamingStrategy +import com.fasterxml.jackson.databind.annotation.JsonNaming +import dev.afanasev.sekret.Secret + +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) +data class ServerSettings ( + val imageServer: String, + val latestBuild: Int, + val url: String, + val compromised: Boolean, + val tls: TlsCert? +) + +@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) +data class TlsCert ( + val createdAt: String, + @field:Secret val privateKey: String, + @field:Secret val certificate: String +)