1
0
Fork 1
mirror of https://gitlab.com/mangadex-pub/mangadex_at_home.git synced 2024-01-19 02:48:37 +00:00

Merge branch 'jackson' into 'master'

Switch from Gson to Jackson for better null checks and invalid property support

See merge request mangadex/mangadex_at_home!35
This commit is contained in:
carbotaniuman 2020-06-19 20:16:24 +00:00
commit 7a278c0386
19 changed files with 283 additions and 356 deletions

View file

@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- [2020-06-19] Errored out on invalid settings.json tokens [@carbotaniuman]
### Changed ### Changed
- [2020-06-19] Changed default CPU thread count to `4` by [@lflare]. - [2020-06-19] Changed default CPU thread count to `4` by [@lflare].

View file

@ -20,16 +20,16 @@ dependencies {
compileOnly group:"dev.afanasev", name: "sekret-annotation", version: "0.0.3" compileOnly group:"dev.afanasev", name: "sekret-annotation", version: "0.0.3"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 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: "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-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-client-apache", version: "$http_4k_version"
implementation group: "org.http4k", name: "http4k-server-netty", 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" 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:"ch.qos.logback", name: "logback-classic", version: "1.2.1"
implementation group: "org.jetbrains.exposed", name: "exposed-core", version: "$exposed_version" implementation group: "org.jetbrains.exposed", name: "exposed-core", version: "$exposed_version"

View file

@ -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);
}
}

View file

@ -3,6 +3,7 @@ package mdnet.base;
import mdnet.base.settings.ClientSettings; import mdnet.base.settings.ClientSettings;
import mdnet.base.server.ApplicationKt; import mdnet.base.server.ApplicationKt;
import mdnet.base.server.WebUiKt; import mdnet.base.server.WebUiKt;
import mdnet.base.settings.ServerSettings;
import mdnet.cache.DiskLruCache; import mdnet.cache.DiskLruCache;
import org.http4k.server.Http4kServer; import org.http4k.server.Http4kServer;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -19,7 +20,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static mdnet.base.Constants.GSON; import static mdnet.base.Constants.JACKSON;
public class MangaDexClient { public class MangaDexClient {
private final static Logger LOGGER = LoggerFactory.getLogger(MangaDexClient.class); private final static Logger LOGGER = LoggerFactory.getLogger(MangaDexClient.class);
@ -62,8 +63,8 @@ public class MangaDexClient {
DiskLruCache.Snapshot snapshot = cache.get("statistics"); DiskLruCache.Snapshot snapshot = cache.get("statistics");
if (snapshot != null) { if (snapshot != null) {
String json = snapshot.getString(0); statistics.set(JACKSON.readValue(snapshot.getInputStream(0), Statistics.class));
statistics.set(GSON.fromJson(json, Statistics.class)); snapshot.close();
} else { } else {
statistics.set(new Statistics()); statistics.set(new Statistics());
} }
@ -215,8 +216,7 @@ public class MangaDexClient {
DiskLruCache.Editor editor = cache.edit("statistics"); DiskLruCache.Editor editor = cache.edit("statistics");
if (editor != null) { if (editor != null) {
String json = GSON.toJson(statistics.get(), Statistics.class); JACKSON.writeValue(editor.newOutputStream(0), Statistics.class);
editor.setString(0, json);
editor.commit(); editor.commit();
} }
} }

View file

@ -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<String, Object> 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<String, Object> 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<ServerSettings> 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<String, Object> 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<ServerSettings> 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;
}
}
}

View file

@ -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='" + "<hidden>" + '\''
+ ", certificate='" + "<hidden>" + '\'' + '}';
}
@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;
}
}
}

View file

@ -1,7 +1,6 @@
package mdnet.base package mdnet.base
import com.google.gson.Gson import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.gson.GsonBuilder
import java.time.Duration import java.time.Duration
object Constants { object Constants {
@ -10,5 +9,5 @@ object Constants {
const val WEBUI_VERSION = "0.1.1" const val WEBUI_VERSION = "0.1.1"
val MAX_AGE_CACHE: Duration = Duration.ofDays(14) val MAX_AGE_CACHE: Duration = Duration.ofDays(14)
@JvmField @JvmField
val GSON: Gson = GsonBuilder().setPrettyPrinting().create() val JACKSON = jacksonObjectMapper()
} }

View file

@ -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<String>) {
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<ClientSettings>(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)
}
}

View file

@ -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<String, Any>(
"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<String, Any> =
mapOf<String, Any>(
"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<Map<String, Any>>().toLens()
private val SERVER_SETTINGS_LENS = Body.auto<ServerSettings>().toLens()
private const val SERVER_ADDRESS = "https://api.mangadex.network/"
}
}

View file

@ -1,12 +1,14 @@
package mdnet.base 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( data class Statistics(
@field:SerializedName("requests_served") val requestsServed: Int = 0, val requestsServed: Int = 0,
@field:SerializedName("cache_hits") val cacheHits: Int = 0, val cacheHits: Int = 0,
@field:SerializedName("cache_misses") val cacheMisses: Int = 0, val cacheMisses: Int = 0,
@field:SerializedName("browser_cached") val browserCached: Int = 0, val browserCached: Int = 0,
@field:SerializedName("bytes_sent") val bytesSent: Long = 0, val bytesSent: Long = 0,
@field:SerializedName("bytes_on_disk") val bytesOnDisk: Long = 0 val bytesOnDisk: Long = 0
) )

View file

@ -1,4 +1,4 @@
package mdnet.base package mdnet.base.netty
import io.netty.bootstrap.ServerBootstrap import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.ChannelFactory 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.stream.ChunkedWriteHandler
import io.netty.handler.traffic.GlobalTrafficShapingHandler import io.netty.handler.traffic.GlobalTrafficShapingHandler
import io.netty.handler.traffic.TrafficCounter import io.netty.handler.traffic.TrafficCounter
import mdnet.base.Statistics
import mdnet.base.settings.ClientSettings import mdnet.base.settings.ClientSettings
import mdnet.base.settings.TlsCert
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import org.http4k.server.Http4kChannelHandler import org.http4k.server.Http4kChannelHandler
import org.http4k.server.Http4kServer import org.http4k.server.Http4kServer
@ -38,7 +40,7 @@ import javax.net.ssl.SSLException
private val LOGGER = LoggerFactory.getLogger("Application") private val LOGGER = LoggerFactory.getLogger("Application")
class Netty(private val tls: ServerSettings.TlsCert, private val clientSettings: ClientSettings, private val statistics: AtomicReference<Statistics>) : ServerConfig { class Netty(private val tls: TlsCert, private val clientSettings: ClientSettings, private val statistics: AtomicReference<Statistics>) : ServerConfig {
override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer { override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer {
private val masterGroup = NioEventLoopGroup(clientSettings.threads) private val masterGroup = NioEventLoopGroup(clientSettings.threads)
private val workerGroup = NioEventLoopGroup(clientSettings.threads) private val workerGroup = NioEventLoopGroup(clientSettings.threads)

View file

@ -19,7 +19,7 @@
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, //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 //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE. //SOFTWARE.
package mdnet.base package mdnet.base.netty
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.security.KeyFactory import java.security.KeyFactory
@ -35,13 +35,23 @@ private const val PKCS_8_PEM_FOOTER = "-----END PRIVATE KEY-----"
fun loadKey(keyDataString: String): PrivateKey? { fun loadKey(keyDataString: String): PrivateKey? {
if (keyDataString.contains(PKCS_1_PEM_HEADER)) { if (keyDataString.contains(PKCS_1_PEM_HEADER)) {
// OpenSSL / PKCS#1 Base64 PEM encoded file // OpenSSL / PKCS#1 Base64 PEM encoded file
val fixedString = keyDataString.replace(PKCS_1_PEM_HEADER, "").replace(PKCS_1_PEM_FOOTER, "") val fixedString = keyDataString.replace(PKCS_1_PEM_HEADER, "").replace(
return readPkcs1PrivateKey(base64Decode(fixedString)) PKCS_1_PEM_FOOTER, "")
return readPkcs1PrivateKey(
base64Decode(
fixedString
)
)
} }
if (keyDataString.contains(PKCS_8_PEM_HEADER)) { if (keyDataString.contains(PKCS_8_PEM_HEADER)) {
// PKCS#8 Base64 PEM encoded file // PKCS#8 Base64 PEM encoded file
val fixedString = keyDataString.replace(PKCS_8_PEM_HEADER, "").replace(PKCS_8_PEM_FOOTER, "") val fixedString = keyDataString.replace(PKCS_8_PEM_HEADER, "").replace(
return readPkcs1PrivateKey(base64Decode(fixedString)) PKCS_8_PEM_FOOTER, "")
return readPkcs1PrivateKey(
base64Decode(
fixedString
)
)
} }
return null return null

View file

@ -1,4 +1,4 @@
package mdnet.base package mdnet.base.netty
import io.netty.bootstrap.ServerBootstrap import io.netty.bootstrap.ServerBootstrap
import io.netty.channel.ChannelFactory import io.netty.channel.ChannelFactory

View file

@ -1,8 +1,8 @@
/* ktlint-disable no-wildcard-imports */ /* ktlint-disable no-wildcard-imports */
package mdnet.base.server package mdnet.base.server
import mdnet.base.Netty import mdnet.base.netty.Netty
import mdnet.base.ServerSettings import mdnet.base.settings.ServerSettings
import mdnet.base.Statistics import mdnet.base.Statistics
import mdnet.base.settings.ClientSettings import mdnet.base.settings.ClientSettings
import mdnet.cache.DiskLruCache 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 database = Database.connect("jdbc:sqlite:cache/data.db", "org.sqlite.JDBC")
val imageServer = ImageServer(cache, statistics, serverSettings.imageServer, database, isHandled) val imageServer = ImageServer(cache, statistics, serverSettings.imageServer, database, isHandled)
return Timer return timeRequest()
.then(catchAllHideDetails()) .then(catchAllHideDetails())
.then(ServerFilters.CatchLensFailure) .then(ServerFilters.CatchLensFailure)
.then(addCommonHeaders()) .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))
} }

View file

@ -200,8 +200,8 @@ class ImageServer(private val cache: DiskLruCache, private val statistics: Atomi
} }
editor.commit() editor.commit()
} else { } else {
if (LOGGER.isInfoEnabled) { if (LOGGER.isWarnEnabled) {
LOGGER.info("Cache download for $sanitizedUri aborted") LOGGER.warn("Cache download for $sanitizedUri aborted")
} }
editor.abort() editor.abort()
} }

View file

@ -4,7 +4,7 @@ package mdnet.base.server
import java.time.Instant import java.time.Instant
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import mdnet.base.Statistics import mdnet.base.Statistics
import mdnet.base.WebUiNetty import mdnet.base.netty.WebUiNetty
import mdnet.base.settings.WebSettings import mdnet.base.settings.WebSettings
import org.http4k.core.Body import org.http4k.core.Body
import org.http4k.core.Method import org.http4k.core.Method
@ -12,7 +12,7 @@ import org.http4k.core.Response
import org.http4k.core.Status import org.http4k.core.Status
import org.http4k.core.then import org.http4k.core.then
import org.http4k.filter.ServerFilters 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.ResourceLoader
import org.http4k.routing.bind import org.http4k.routing.bind
import org.http4k.routing.routes import org.http4k.routing.routes

View file

@ -41,25 +41,24 @@ fun catchAllHideDetails(): Filter {
} }
} }
val Timer = Filter { fun timeRequest(): Filter {
next: HttpHandler -> { return Filter { next: HttpHandler ->
request: Request -> { request: Request ->
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val response = next(request) val response = next(request)
val latency = System.currentTimeMillis() - start val latency = System.currentTimeMillis() - start
if (LOGGER.isTraceEnabled && response.header("X-Uri") != null) { if (LOGGER.isTraceEnabled && response.header("X-Uri") != null) {
// Dirty hack to get sanitizedUri from ImageServer // Dirty hack to get sanitizedUri from ImageServer
val sanitizedUri = response.header("X-Uri") val sanitizedUri = response.header("X-Uri")
// Log in TRACE // Log in TRACE
if (LOGGER.isInfoEnabled) { if (LOGGER.isInfoEnabled) {
LOGGER.info("Request for $sanitizedUri completed in ${latency}ms") LOGGER.info("Request for $sanitizedUri completed in ${latency}ms")
} }
// Delete response header entirely // Delete response header entirely
response.header("X-Uri", null) response.header("X-Uri", null)
} }
// Set response header with processing times // Set response header with processing times
response.header("X-Time-Taken", latency.toString()) response.header("X-Time-Taken", latency.toString())
}
} }
} }

View file

@ -1,20 +1,23 @@
package mdnet.base.settings 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 import dev.afanasev.sekret.Secret
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class ClientSettings( data class ClientSettings(
@field:SerializedName("max_cache_size_in_mebibytes") val maxCacheSizeInMebibytes: Long = 20480, val maxCacheSizeInMebibytes: Long = 20480,
@field:SerializedName("max_mebibytes_per_hour") val maxMebibytesPerHour: Long = 0, val maxMebibytesPerHour: Long = 0,
@field:SerializedName("max_kilobits_per_second") val maxKilobitsPerSecond: Long = 0, val maxKilobitsPerSecond: Long = 0,
@field:SerializedName("client_hostname") val clientHostname: String = "0.0.0.0", val clientHostname: String = "0.0.0.0",
@field:SerializedName("client_port") val clientPort: Int = 443, val clientPort: Int = 443,
@field:Secret @field:SerializedName("client_secret") val clientSecret: String = "PASTE-YOUR-SECRET-HERE", @field:Secret val clientSecret: String = "PASTE-YOUR-SECRET-HERE",
@field:SerializedName("threads") val threads: Int = 4, val threads: Int = 4,
@field:SerializedName("web_settings") val webSettings: WebSettings? = null val webSettings: WebSettings? = null
) )
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class WebSettings( data class WebSettings(
@field:SerializedName("ui_hostname") val uiHostname: String = "127.0.0.1", val uiHostname: String = "127.0.0.1",
@field:SerializedName("ui_port") val uiPort: Int = 8080 val uiPort: Int = 8080
) )

View file

@ -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
)