diff --git a/settings.sample.yaml b/settings.sample.yaml index 2ec18ec..f072b2d 100644 --- a/settings.sample.yaml +++ b/settings.sample.yaml @@ -66,6 +66,11 @@ server_settings: # The external hostname to listen on # Keep this at 0.0.0.0 unless you know what you're doing hostname: 0.0.0.0 + # The external ip to broadcast to the webserver + # The default of null means the backend will infer it + # from where it was sent from, which may fail in the + # presence of multiple IPs + external_ip: ~ # Maximum mebibytes per hour of images to server # Setting this to 0 disables the limiter max_mebibytes_per_hour: 0 diff --git a/src/main/kotlin/mdnet/BackendApi.kt b/src/main/kotlin/mdnet/BackendApi.kt index 6d30b02..ba4e9c7 100644 --- a/src/main/kotlin/mdnet/BackendApi.kt +++ b/src/main/kotlin/mdnet/BackendApi.kt @@ -22,22 +22,17 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.KotlinModule import mdnet.ServerHandlerJackson.auto import mdnet.logging.info -import mdnet.settings.ClientSettings -import mdnet.settings.RemoteSettings -import org.apache.hc.client5.http.impl.DefaultSchemePortResolver -import org.apache.hc.client5.http.impl.classic.HttpClients -import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner -import org.apache.hc.core5.http.HttpHost -import org.apache.hc.core5.http.protocol.HttpContext +import mdnet.settings.* import org.http4k.client.ApacheClient import org.http4k.core.Body +import org.http4k.core.HttpHandler 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.http4k.lens.LensFailure import org.slf4j.LoggerFactory -import java.net.InetAddress object ServerHandlerJackson : ConfigurableJackson( KotlinModule() @@ -49,94 +44,88 @@ object ServerHandlerJackson : ConfigurableJackson( class BackendApi(private val settings: ClientSettings) { private val serverAddress = settings.devSettings.devUrl ?: SERVER_ADDRESS - private val client = ApacheClient( - client = HttpClients.custom() - .setRoutePlanner( - object : DefaultRoutePlanner(DefaultSchemePortResolver()) { - override fun determineLocalAddress(firstHop: HttpHost?, context: HttpContext?): InetAddress { - return InetAddress.getByName(settings.serverSettings.hostname) - } - } - ) - .build() - ) + private val client = ApacheClient() fun logoutFromControl(): Boolean { + val serverSettings = settings.serverSettings + LOGGER.info { "Disconnecting from the control server" } - val params = mapOf( - "secret" to settings.serverSettings.secret + + val request = LOGOUT_REQUEST_LENS( + LogoutRequest(serverSettings.secret), + Request(Method.POST, serverAddress + "stop") ) - val request = STRING_ANY_MAP_LENS(params, Request(Method.POST, serverAddress + "stop")) val response = client(request) return response.status.successful } - private fun getPingParams(tlsCreatedAt: String? = null): Map { + private fun getPingParams(tlsCreatedAt: String? = null): SettingsRequest { val serverSettings = settings.serverSettings - return mapOf( - "secret" to serverSettings.secret, - "port" to let { - if (serverSettings.externalPort != 0) { - serverSettings.externalPort - } else { - serverSettings.port - } - }, - "disk_space" to settings.maxCacheSizeInMebibytes * 1024 * 1024, - "network_speed" to serverSettings.externalMaxKilobitsPerSecond * 1000 / 8, - "build_version" to Constants.CLIENT_BUILD - ).let { - if (tlsCreatedAt != null) { - it.plus("tls_created_at" to tlsCreatedAt) + return SettingsRequest( + secret = serverSettings.secret, + port = if (serverSettings.externalPort != 0) { + serverSettings.externalPort } else { - it - } - } + serverSettings.port + }, + buildVersion = Constants.CLIENT_BUILD, + diskSpace = settings.maxCacheSizeInMebibytes * 1024 * 1024, + networkSpeed = serverSettings.externalMaxKilobitsPerSecond * 1000 / 8, + ipAddress = serverSettings.externalIp, + tlsCreatedAt = tlsCreatedAt, + ) } - fun loginToControl(): RemoteSettings? { + fun loginToControl(): PingResult { LOGGER.info { "Connecting to the control server" } - val request = STRING_ANY_MAP_LENS( + val request = SETTINGS_REQUEST_LENS( getPingParams(null), Request( Method.POST, serverAddress + "ping" ) ) - val response = client(request) - - return if (response.status.successful) { - SERVER_SETTINGS_LENS(response) - } else { - null - } + return client.makeRequest(request) } - fun pingControl(old: RemoteSettings): RemoteSettings? { + fun pingControl(old: RemoteSettings): PingResult { LOGGER.info { "Pinging the control server" } - val request = STRING_ANY_MAP_LENS( + val request = SETTINGS_REQUEST_LENS( getPingParams(old.tls!!.createdAt), Request( Method.POST, serverAddress + "ping" ) ) - val response = client(request) + return client.makeRequest(request) + } - return if (response.status.successful) { - SERVER_SETTINGS_LENS(response) - } else { - null + private fun HttpHandler.makeRequest(request: Request): PingResult { + val response = this(request) + + return when { + response.status.successful -> { + SERVER_SETTINGS_LENS(response) + } + else -> { + try { + PING_FAILURE_LENS(response) + } catch (e: LensFailure) { + PingFailure(response.status.code, response.status.description) + } + } } } companion object { private val LOGGER = LoggerFactory.getLogger(BackendApi::class.java) - private val STRING_ANY_MAP_LENS = Body.auto>().toLens() + private val SETTINGS_REQUEST_LENS = Body.auto().toLens() + private val PING_FAILURE_LENS = Body.auto().toLens() + private val LOGOUT_REQUEST_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/Main.kt b/src/main/kotlin/mdnet/Main.kt index c9487e6..f3f2a4c 100644 --- a/src/main/kotlin/mdnet/Main.kt +++ b/src/main/kotlin/mdnet/Main.kt @@ -23,7 +23,6 @@ import mdnet.logging.error import org.slf4j.LoggerFactory import picocli.CommandLine import java.io.File -import java.lang.Exception import java.nio.file.Path import java.nio.file.Paths import kotlin.system.exitProcess @@ -88,6 +87,7 @@ class ClientArgs( val client = MangaDexClient(settingsFile, databaseFile, cacheFolder) val hook = Thread { + println("A") client.shutdown() (LoggerFactory.getILoggerFactory() as LoggerContext).stop() } @@ -97,7 +97,7 @@ class ClientArgs( try { client.runLoop() - } catch (e: Exception) { + } catch (e: Throwable) { Runtime.getRuntime().removeShutdownHook( hook ) diff --git a/src/main/kotlin/mdnet/ServerManager.kt b/src/main/kotlin/mdnet/ServerManager.kt index 4faef83..0e495a3 100644 --- a/src/main/kotlin/mdnet/ServerManager.kt +++ b/src/main/kotlin/mdnet/ServerManager.kt @@ -189,7 +189,7 @@ class ServerManager( val state = this.state as Running val newSettings = backendApi.pingControl(state.settings) - if (newSettings != null) { + if (newSettings is RemoteSettings) { LOGGER.info { "Server settings received: $newSettings" } warnBasedOnSettings(newSettings) @@ -201,7 +201,7 @@ class ServerManager( } } } else { - LOGGER.info { "Server ping failed - ignoring" } + LOGGER.info { "Ignoring failed server ping - $newSettings" } } } @@ -209,7 +209,9 @@ class ServerManager( this.state as Uninitialized val remoteSettings = backendApi.loginToControl() - ?: throw RuntimeException("Failed to get a login response from server") + if (remoteSettings !is RemoteSettings) { + throw RuntimeException(remoteSettings.toString()) + } LOGGER.info { "Server settings received: $remoteSettings" } warnBasedOnSettings(remoteSettings) diff --git a/src/main/kotlin/mdnet/settings/ClientSettings.kt b/src/main/kotlin/mdnet/settings/ClientSettings.kt index ffb7701..5f416e6 100644 --- a/src/main/kotlin/mdnet/settings/ClientSettings.kt +++ b/src/main/kotlin/mdnet/settings/ClientSettings.kt @@ -40,6 +40,7 @@ data class ServerSettings( val maxKilobitsPerSecond: Long = 0, val externalMaxKilobitsPerSecond: Long = 0, val maxMebibytesPerHour: Long = 0, + val externalIp: String? = null, val port: Int = 443, val threads: Int = 0, ) diff --git a/src/main/kotlin/mdnet/settings/RemoteSettings.kt b/src/main/kotlin/mdnet/settings/PingFailure.kt similarity index 93% rename from src/main/kotlin/mdnet/settings/RemoteSettings.kt rename to src/main/kotlin/mdnet/settings/PingFailure.kt index ac49220..e1f4faf 100644 --- a/src/main/kotlin/mdnet/settings/RemoteSettings.kt +++ b/src/main/kotlin/mdnet/settings/PingFailure.kt @@ -23,6 +23,14 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming import dev.afanasev.sekret.Secret import org.http4k.core.Uri +sealed class PingResult + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) +data class PingFailure( + val status: Int, + val error: String, +) : PingResult() + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) data class RemoteSettings( val imageServer: Uri, @@ -33,7 +41,7 @@ data class RemoteSettings( val paused: Boolean, val forceDisableTokens: Boolean = false, val tls: TlsCert? -) { +) : PingResult() { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/src/main/kotlin/mdnet/settings/SettingsRequest.kt b/src/main/kotlin/mdnet/settings/SettingsRequest.kt new file mode 100644 index 0000000..9a88046 --- /dev/null +++ b/src/main/kotlin/mdnet/settings/SettingsRequest.kt @@ -0,0 +1,39 @@ +/* +Mangadex@Home +Copyright (c) 2020, MangaDex Network +This file is part of MangaDex@Home. + +MangaDex@Home is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +MangaDex@Home is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this MangaDex@Home. If not, see . +*/ +package mdnet.settings + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import dev.afanasev.sekret.Secret + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) +data class SettingsRequest( + @field:Secret val secret: String, + val ipAddress: String?, + val port: Int, + val diskSpace: Long, + val networkSpeed: Long, + val buildVersion: Int, + val tlsCreatedAt: String?, +) + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) +data class LogoutRequest( + @field:Secret val secret: String, +)