From baedf9ba1687dd74139d765564d77fcfd7e022dc Mon Sep 17 00:00:00 2001 From: ProtoByter Date: Tue, 29 Nov 2022 18:49:10 +0000 Subject: [PATCH] HEHE LOTS OF WOOOOORK --- .../openproximitychat/common/Logging.kt | 1 - .../openproximitychat/common/UPnPManager.kt | 3 +- .../fabric/connect/Handshake.java | 16 +- .../fabric/connect/HandshakeBuilder.java | 9 +- .../mixins/ChatMessageS2CPacketMixin.java | 26 +- .../mixins/OtherClientPlayerEntityMixin.java | 21 +- server/.gitignore | 36 +++ server/build.gradle.kts | 82 ++++++ server/gradle.properties | 9 + server/gradlew | 234 ++++++++++++++++++ server/gradlew.bat | 89 +++++++ .../openproximitychat/Application.kt | 81 ++++++ .../openproximitychat/DatabaseHandler.kt | 89 +++++++ .../openproximitychat/InstantSerialiser.kt | 28 +++ .../openproximitychat/Requests.kt | 40 +++ .../openproximitychat/UUIDSerialise.kt | 27 ++ .../openproximitychat/Versions.kt | 35 +++ server/src/main/resources/logback.xml | 12 + server/src/main/resources/versions.txt | 7 + .../openproximitychat/ApplicationKtTest.kt | 101 ++++++++ .../openproximitychat/DatabaseHandlerTest.kt | 85 +++++++ settings.gradle.kts | 2 +- 22 files changed, 1000 insertions(+), 33 deletions(-) create mode 100644 server/.gitignore create mode 100644 server/build.gradle.kts create mode 100644 server/gradle.properties create mode 100755 server/gradlew create mode 100644 server/gradlew.bat create mode 100644 server/src/main/kotlin/org/muellerssoftware/openproximitychat/Application.kt create mode 100644 server/src/main/kotlin/org/muellerssoftware/openproximitychat/DatabaseHandler.kt create mode 100644 server/src/main/kotlin/org/muellerssoftware/openproximitychat/InstantSerialiser.kt create mode 100644 server/src/main/kotlin/org/muellerssoftware/openproximitychat/Requests.kt create mode 100644 server/src/main/kotlin/org/muellerssoftware/openproximitychat/UUIDSerialise.kt create mode 100644 server/src/main/kotlin/org/muellerssoftware/openproximitychat/Versions.kt create mode 100644 server/src/main/resources/logback.xml create mode 100644 server/src/main/resources/versions.txt create mode 100644 server/src/test/kotlin/org/muellerssoftware/openproximitychat/ApplicationKtTest.kt create mode 100644 server/src/test/kotlin/org/muellerssoftware/openproximitychat/DatabaseHandlerTest.kt diff --git a/common/src/main/kotlin/org/muellerssoftware/openproximitychat/common/Logging.kt b/common/src/main/kotlin/org/muellerssoftware/openproximitychat/common/Logging.kt index 74c0c1c..de306a5 100644 --- a/common/src/main/kotlin/org/muellerssoftware/openproximitychat/common/Logging.kt +++ b/common/src/main/kotlin/org/muellerssoftware/openproximitychat/common/Logging.kt @@ -8,7 +8,6 @@ object Logging { Info, Error } - var logger: Logger? = null set(value) { for (log in backLog) { diff --git a/common/src/main/kotlin/org/muellerssoftware/openproximitychat/common/UPnPManager.kt b/common/src/main/kotlin/org/muellerssoftware/openproximitychat/common/UPnPManager.kt index 4b1fa6d..b9a10c6 100644 --- a/common/src/main/kotlin/org/muellerssoftware/openproximitychat/common/UPnPManager.kt +++ b/common/src/main/kotlin/org/muellerssoftware/openproximitychat/common/UPnPManager.kt @@ -36,6 +36,7 @@ object UPnPManager { Logging.info("UPnP gateway found: ${gateway!!.friendlyName}") val ports = mappedPorts.toMutableMap() mappedPorts.clear() + ports.map { mapPort(it.key, it.value) } @@ -51,7 +52,7 @@ object UPnPManager { mappedPorts.put(internalPort, externalPort) } - val succeeded = gateway?.addPortMapping(externalPort, internalPort, gateway!!.localAddress.hostAddress, "UDP", "OpenProximityChat mapped port") + val succeeded = gateway?.addPortMapping(externalPort, internalPort, gateway!!.localAddress.hostAddress, "TCP", "OpenProximityChat mapped port") if (succeeded == true) { mappedPorts.put(internalPort, externalPort) } diff --git a/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/connect/Handshake.java b/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/connect/Handshake.java index 0f8698b..45275ad 100644 --- a/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/connect/Handshake.java +++ b/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/connect/Handshake.java @@ -1,18 +1,18 @@ package org.muellerssoftware.openproximitychat.fabric.connect; -import net.minecraft.entity.player.PlayerEntity; -import org.muellerssoftware.openproximitychat.common.VoiceServer; import org.muellerssoftware.openproximitychat.common.VoiceClient; +import org.muellerssoftware.openproximitychat.common.VoiceServer; import java.net.InetSocketAddress; +import java.util.UUID; public class Handshake { - Handshake(ConnectionFilterType type, PlayerEntity player) { + Handshake(ConnectionFilterType type, UUID player) { this.filterType = type; this.player = player; } private final ConnectionFilterType filterType; - private final PlayerEntity player; + private final UUID player; private InetSocketAddress address; private VoiceClient voiceClient; @@ -20,7 +20,7 @@ public class Handshake { this.address = address; } - public PlayerEntity getPlayer() { + public UUID getPlayer() { return player; } @@ -29,9 +29,7 @@ public class Handshake { } public void connect() { - - - VoiceServer.INSTANCE.getHandshook().put(player.getUuid(), address.getAddress()); - voiceClient = new VoiceClient(new InetSocketAddress(address.getHostName(), address.getPort()), player.getUuid()); + VoiceServer.INSTANCE.getHandshook().put(player, address.getAddress()); + voiceClient = new VoiceClient(new InetSocketAddress(address.getHostName(), address.getPort()), player); } } diff --git a/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/connect/HandshakeBuilder.java b/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/connect/HandshakeBuilder.java index 2b9e4c3..751af20 100644 --- a/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/connect/HandshakeBuilder.java +++ b/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/connect/HandshakeBuilder.java @@ -7,6 +7,7 @@ import org.muellerssoftware.openproximitychat.fabric.OPCFabric; import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; +import java.util.UUID; public class HandshakeBuilder { public static class HandshakeMovement { @@ -104,6 +105,10 @@ public class HandshakeBuilder { @Nullable public Handshake tryBuild() throws HandshakeException { + if (handshake != null) { + return handshake; + } + if (this.movements.size() < 24) { throw new HandshakeException("Too few stored head movements (need at least 24)"); } @@ -121,7 +126,7 @@ public class HandshakeBuilder { if (handshakeMovements != null) { OPCFabric.LOGGER.info("Found handshake in {}'s head movements", this.player.getName().asTruncatedString(64)); ConnectionFilterType filterType; - PlayerEntity player; + UUID player; if (handshakeMovements.get(5).equals(FRIENDSHIP_MOVE)) { filterType = ConnectionFilterType.Friend; } else if (handshakeMovements.get(5).equals(OMNIFRIEND_MOVE)) { @@ -131,7 +136,7 @@ public class HandshakeBuilder { } if (this.player != null) { - player = this.player; + player = this.player.getUuid(); handshake = new Handshake(filterType, player); } else { throw new HandshakeException("Player not set in Stage 1 Handshake"); diff --git a/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/mixins/ChatMessageS2CPacketMixin.java b/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/mixins/ChatMessageS2CPacketMixin.java index 96fdac5..0105ac6 100644 --- a/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/mixins/ChatMessageS2CPacketMixin.java +++ b/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/mixins/ChatMessageS2CPacketMixin.java @@ -26,9 +26,29 @@ public class ChatMessageS2CPacketMixin { assert MinecraftClient.getInstance().player != null; if (sender.equals(MinecraftClient.getInstance().player.getUuid())) return; + if (messageContent.startsWith("OPCAck")) { + OPCFabric.LOGGER.info("Received OPCAck from {}", sender); + + + + if (messageContent.length() != 7) { + OPCFabric.LOGGER.error("Received invalid OPCAck from {}", sender); + } else { + if (messageContent.toCharArray()[6] == 'O') { + + } else if (messageContent.toCharArray()[6] == 'F') { + + } else { + OPCFabric.LOGGER.error("Received invalid OPCAck from {}", sender); + } + } + + //Handshake handshake = OPCFabric.tracked_players.get(sender); + } + if (OPCFabric.tracked_players.containsKey(sender)) { - if (messageContent.matches("(\\b25[0-5]|\\b2[0-4][0-9]|\\b[01]?[0-9][0-9]?)(\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))")) { // Massive regex to check if the message is a valid IP address - OPCFabric.LOGGER.info("Found IP address in message from player " + sender + ": " + messageContent); + if (messageContent.matches("OPC (\\b25[0-5]|\\b2[0-4][0-9]|\\b[01]?[0-9][0-9]?)(\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}:((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))")) { // Massive regex to check if the message is a valid IP address + OPCFabric.LOGGER.info("Found IP address in message from player {}: {}", sender, messageContent); try { OPCFabric.tracked_players.get(sender).tryBuild().addAddress(InetSocketAddress.createUnresolved(messageContent.split(":")[0], Integer.parseInt(messageContent.split(":")[1]))); } catch (Exception e) { @@ -36,7 +56,5 @@ public class ChatMessageS2CPacketMixin { } } } - - OPCFabric.LOGGER.info(((ChatMessageS2CPacket)(Object)this).message().createMetadata().sender().toString()); } } diff --git a/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/mixins/OtherClientPlayerEntityMixin.java b/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/mixins/OtherClientPlayerEntityMixin.java index d545916..63800e9 100644 --- a/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/mixins/OtherClientPlayerEntityMixin.java +++ b/fabric/src/main/java/org/muellerssoftware/openproximitychat/fabric/mixins/OtherClientPlayerEntityMixin.java @@ -6,16 +6,10 @@ import org.muellerssoftware.openproximitychat.fabric.OPCFabric; import org.muellerssoftware.openproximitychat.fabric.connect.Handshake; import org.muellerssoftware.openproximitychat.fabric.connect.HandshakeBuilder; import org.muellerssoftware.openproximitychat.fabric.connect.HandshakeException; -import org.muellerssoftware.openproximitychat.common.UPnPManager; -import org.muellerssoftware.openproximitychat.common.VoiceServer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.xml.sax.SAXException; - -import java.io.IOException; -import java.util.Objects; @Mixin(net.minecraft.client.network.OtherClientPlayerEntity.class) public class OtherClientPlayerEntityMixin { @@ -30,29 +24,26 @@ public class OtherClientPlayerEntityMixin { if (!builder.built) { try { builder.add(new HandshakeBuilder.HandshakeMovement(accessor.getServerHeadYaw(), accessor.getServerPitch()).convertToHandshakeForm()); - } catch (IllegalArgumentException ignored) { - } + } catch (IllegalArgumentException ignored) {} + if (builder.readyToBuild()) { try { Handshake handshake = builder.tryBuild(); if (handshake != null) { - OPCFabric.LOGGER.info("Found Handshake for player {}", handshake.getPlayer().getName()); + OPCFabric.LOGGER.info("Found Handshake for player {}", player.getName().getString()); } if (!builder.built) { assert handshake != null; - OPCFabric.LOGGER.info("Handshake for player {} was built, connecting", handshake.getPlayer().getName()); + OPCFabric.LOGGER.info("Handshake for player {} was built, connecting", player.getName().getString()); assert MinecraftClient.getInstance().player != null; - MinecraftClient.getInstance().player.sendCommand("msg " + handshake.getPlayer().getName().getString() + " " + Objects.requireNonNull(UPnPManager.INSTANCE.getGateway()).getExternalIPAddress() + ":" + VoiceServer.INSTANCE.getPort(), null); + // TODO: Implement friend system + MinecraftClient.getInstance().player.sendCommand("msg " + player.getName().getString() + " OPCAckO", null); builder.built = true; } } catch (HandshakeException e) { OPCFabric.LOGGER.error("Caught HandshakeException ({})", e.getMessage()); - } catch (IOException e) { - OPCFabric.LOGGER.error("Caught IOException ({})", e.getMessage()); - } catch (SAXException e) { - OPCFabric.LOGGER.error("Caught SAXException ({})", e.getMessage()); } } } diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..c426c32 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,36 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/server/build.gradle.kts b/server/build.gradle.kts new file mode 100644 index 0000000..68beb59 --- /dev/null +++ b/server/build.gradle.kts @@ -0,0 +1,82 @@ +val ktor_version: String by project +val kotlin_version: String by project +val logback_version: String by project +val exposed_version: String by project +val weupnp_version: String by project +val db_version: String by project +val junit_version: String by project + +plugins { + application + kotlin("jvm") version "1.7.21" + id("io.ktor.plugin") version "2.1.3" + id("org.jetbrains.kotlin.plugin.serialization") version "1.7.21" +} + +group = "org.muellerssoftware.openproximitychat" +version = "0.0.1" + +application { + mainClass.set("org.muellerssoftware.openproximitychat.ApplicationKt") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +dependencies { + // JUnit + testImplementation(platform("org.junit:junit-bom:$junit_version")) + testImplementation("org.junit.jupiter:junit-jupiter") + + // Ktor + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") + implementation("io.ktor:ktor-server-locations-jvm:$ktor_version") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") + implementation("io.ktor:ktor-server-auth-jvm:$ktor_version") + implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") + implementation("ch.qos.logback:logback-classic:$logback_version") + testImplementation("io.ktor:ktor-server-tests-jvm:$ktor_version") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") + + // UPNP + implementation("org.bitlet:weupnp:$weupnp_version") + + // Jetbrains Exposed + implementation("org.jetbrains.exposed:exposed-core:$exposed_version") + implementation("org.jetbrains.exposed:exposed-dao:$exposed_version") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") + implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version") + + // H2 + implementation("com.h2database:h2:$db_version") +} + +tasks { + test { + testLogging { + events("passed", "skipped", "failed") + } + } + + processResources { + filesMatching("versions.txt") { + expand( + mapOf( + "ktor_version" to ktor_version, + "logback_version" to logback_version, + "exposed_version" to exposed_version, + "weupnp_version" to weupnp_version, + "project_version" to project.version, + "db_version" to db_version, + "junit_version" to junit_version + ) + ) + } + } +} \ No newline at end of file diff --git a/server/gradle.properties b/server/gradle.properties new file mode 100644 index 0000000..01cce2e --- /dev/null +++ b/server/gradle.properties @@ -0,0 +1,9 @@ +ktor_version=2.1.3 +kotlin_version=1.7.21 +logback_version=1.4.5 +exposed_version=0.41.1 +weupnp_version=0.1.4 +db_version=2.1.214 +junit_version=5.9.1 + +kotlin.code.style=official diff --git a/server/gradlew b/server/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/server/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/server/gradlew.bat b/server/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/server/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Application.kt b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Application.kt new file mode 100644 index 0000000..bd150f3 --- /dev/null +++ b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Application.kt @@ -0,0 +1,81 @@ +package org.muellerssoftware.openproximitychat + +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import java.util.* + +fun main() { + embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module) + .start(wait = true) +} + +fun Application.module() { + routing { + get("/info") { + call.respondText(""" + OpenProximityChat Tracker Server ${Versions.project} + Made possible by the OpenGamers project + Running on Java ${Versions.java} + Written in Kotlin ${Versions.kotlin} and Ktor ${Versions.ktor} + Using the ${DatabaseHandler.db!!.dialect.name} Database ${Versions.db} in the backend via Exposed ${Versions.exposed} + Logging handled by Logback ${Versions.logback} + UPnP handled by WeUPnP ${Versions.weupnp} + Tested with JUnit ${Versions.junit} + + Written for MSD by Kai (protobyte@duck.com) + """.trimIndent() + ) + } + + post("/register") { + var request: RegisterRequest? = null + + try { + request = call.receive() + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, "Invalid request") + } + } + + post("/heartbeat") { + + } + + post("/exit") { + var request: ExitRequest? = null + + try { + request = call.receive() + } catch (e: ContentTransformationException) { + return@post call.respondText("Invalid body", status = HttpStatusCode.BadRequest) + } + + try { + if (DatabaseHandler.checkCredentials( + request!!.id, + Base64.getDecoder().decode(request!!.password_hash) + ) + ) { + if (DatabaseHandler.removeClient(request!!.id)) { + call.respondText("Client removed") + } + } else { + call.respondText("Client not found") + } + } catch (e: Exception) { + call.respondText("Client not found") + } + } + } + + install(ContentNegotiation) { + json() + } +} diff --git a/server/src/main/kotlin/org/muellerssoftware/openproximitychat/DatabaseHandler.kt b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/DatabaseHandler.kt new file mode 100644 index 0000000..029d57c --- /dev/null +++ b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/DatabaseHandler.kt @@ -0,0 +1,89 @@ +@file:UseSerializers(UUIDAsStringSerializer::class, InstantAsStringSerializer::class) + +package org.muellerssoftware.openproximitychat + +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import org.h2.security.SHA3 +import org.jetbrains.exposed.dao.id.UUIDTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.javatime.timestamp +import org.jetbrains.exposed.sql.transactions.transaction +import java.time.Instant +import java.util.* +import kotlin.random.Random + +@Serializable +class Client( + var uuid: UUID, + var name: String, + var timeout: Instant, + var ip: String, + var password_hash: ByteArray +) + +object Clients: UUIDTable() { + var name = varchar("name", 16) + var ip = varchar("ip", 21) + var timeout = timestamp("timeout") + var password_hash = binary("password_hash", 96) +} + +object DatabaseHandler { + var db: Database? = null + + init { + db = Database.connect("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;", "org.h2.Driver", "sa", "") + + transaction { + SchemaUtils.create(Clients) + } + } + + fun addClient(id: UUID, name: String, ip: String): ByteArray { + val password = Random.nextBytes(128) + val sha3 = SHA3.getSha3_512() + val passwordHash = Base64.getEncoder().encode(sha3.digest(password)) + + transaction { + Clients.insert { + it[Clients.id] = id + it[Clients.name] = name + it[Clients.ip] = ip + it[password_hash] = passwordHash + it[timeout] = java.time.LocalDateTime.now().toInstant(java.time.ZoneOffset.UTC) + java.time.Duration.ofMinutes(5) + } + } + + return password + } + + fun getClient(name: String): Client? { + return transaction { + Clients.select { Clients.name eq name }.map { + Client(it[Clients.id].value, it[Clients.name], it[Clients.timeout], it[Clients.ip], it[Clients.password_hash]) + }.firstOrNull() + } + } + + fun removeClient(id: UUID): Boolean { + return transaction { + return@transaction Clients.deleteWhere { Clients.id eq id } > 0 + } + } + + fun checkCredentials(id: UUID, password_hash: ByteArray): Boolean { + return transaction { + return@transaction Clients.select { Clients.id eq id }.map { + it[Clients.password_hash] + }.firstOrNull().contentEquals(password_hash) + } + } + + internal fun clear() { + transaction { + Clients.deleteAll() + } + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/org/muellerssoftware/openproximitychat/InstantSerialiser.kt b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/InstantSerialiser.kt new file mode 100644 index 0000000..4146769 --- /dev/null +++ b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/InstantSerialiser.kt @@ -0,0 +1,28 @@ +package org.muellerssoftware.openproximitychat + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.time.Instant +import java.util.* + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = Instant::class) +object InstantAsStringSerializer: KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantAsStringSerializer", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Instant) { + val string = value.toString() + encoder.encodeString(string) + } + + override fun deserialize(decoder: Decoder): Instant { + val string = decoder.decodeString() + return Instant.parse(string) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Requests.kt b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Requests.kt new file mode 100644 index 0000000..eeeb05f --- /dev/null +++ b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Requests.kt @@ -0,0 +1,40 @@ +@file:UseSerializers(UUIDAsStringSerializer::class) + +package org.muellerssoftware.openproximitychat + +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import java.util.* + +@Serializable +data class RegisterRequest( + val id: UUID, + val name: String, + val ip: String, +) + +@Serializable +data class RegisterResponse( + val password: String, +) + +@Serializable +data class HeartbeatRequest( + val id: UUID, +) + +@Serializable +data class ExitRequest( + val id: UUID, + val password_hash: String, +) + +@Serializable +data class SearchRequest( + val id: UUID, +) + +@Serializable +data class SearchResponse( + val clients: List +) \ No newline at end of file diff --git a/server/src/main/kotlin/org/muellerssoftware/openproximitychat/UUIDSerialise.kt b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/UUIDSerialise.kt new file mode 100644 index 0000000..977eb68 --- /dev/null +++ b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/UUIDSerialise.kt @@ -0,0 +1,27 @@ +package org.muellerssoftware.openproximitychat + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.* + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = UUID::class) +object UUIDAsStringSerializer: KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUIDAsStringSerializer", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: UUID) { + val string = value.toString() + encoder.encodeString(string) + } + + override fun deserialize(decoder: Decoder): UUID { + val string = decoder.decodeString() + return UUID.fromString(string) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Versions.kt b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Versions.kt new file mode 100644 index 0000000..4ab6617 --- /dev/null +++ b/server/src/main/kotlin/org/muellerssoftware/openproximitychat/Versions.kt @@ -0,0 +1,35 @@ +package org.muellerssoftware.openproximitychat + +import io.ktor.server.application.* + +object Versions { + var ktor = "" + var logback = "" + var exposed = "" + var weupnp = "" + var project = "" + var db = "" + var junit = "" + val kotlin = "${KotlinVersion.CURRENT}" + val java = System.getProperty("java.version") + + init { + Application::class.java.getResource("/versions.txt")!!.openStream().use { + it.bufferedReader().useLines { lines -> + + lines.forEach { + val split = it.split("=") + when (split[0]) { + "ktor_version" -> ktor = split[1].trim() + "logback_version" -> logback = split[1].trim() + "exposed_version" -> exposed = split[1].trim() + "weupnp_version" -> weupnp = split[1].trim() + "project_version" -> project = split[1].trim() + "junit_version" -> junit = split[1].trim() + "db_version" -> db = split[1].trim() + } + } + } + } + } +} \ No newline at end of file diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml new file mode 100644 index 0000000..bdbb64e --- /dev/null +++ b/server/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + diff --git a/server/src/main/resources/versions.txt b/server/src/main/resources/versions.txt new file mode 100644 index 0000000..f28bb31 --- /dev/null +++ b/server/src/main/resources/versions.txt @@ -0,0 +1,7 @@ +project_version=$project_version +ktor_version=$ktor_version +exposed_version=$exposed_version +logback_version=$logback_version +weupnp_version=$weupnp_version +db_version=$db_version +junit_version=$junit_version \ No newline at end of file diff --git a/server/src/test/kotlin/org/muellerssoftware/openproximitychat/ApplicationKtTest.kt b/server/src/test/kotlin/org/muellerssoftware/openproximitychat/ApplicationKtTest.kt new file mode 100644 index 0000000..0d61a98 --- /dev/null +++ b/server/src/test/kotlin/org/muellerssoftware/openproximitychat/ApplicationKtTest.kt @@ -0,0 +1,101 @@ +package org.muellerssoftware.openproximitychat + +import io.ktor.client.call.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.testing.* +import org.junit.Before +import java.util.* +import kotlin.test.Test +import kotlin.test.assertEquals + +class ApplicationKtTest { + @Before + fun addTestData() { + DatabaseHandler.clear() + DatabaseHandler.addClient(UUID.fromString("00000000-0000-0000-0000-000000000001"), "TestClient1", "0.0.0.0:1") + } + + @Test + fun testPostExitInvalidID() = testApplication { + val client = createClient { + install(ContentNegotiation) { + json() + } + } + + application { + module() + } + + addTestData() + val pw_hash = DatabaseHandler.getClient("TestClient1")?.password_hash + + client.post("/exit") { + contentType(ContentType.Application.Json) + setBody(ExitRequest(UUID.fromString("00000000-0000-0000-0000-000000000000"), Base64.getEncoder().encodeToString(pw_hash))) + }.apply { + assertEquals("Client not found", body()) + } + } + + @Test + fun testPostExitInvalidPw() = testApplication { + val client = createClient { + install(ContentNegotiation) { + json() + } + } + + application { + module() + } + + addTestData() + + client.post("/exit") { + contentType(ContentType.Application.Json) + setBody(ExitRequest(UUID.fromString("00000000-0000-0000-0000-000000000001"), "heheh")) + }.apply { + assertEquals("Client not found", body()) + } + } + + + @Test + fun testPostExitValidID() = testApplication { + val client = createClient { + install(ContentNegotiation) { + json() + } + } + + application { + module() + } + + addTestData() + + val pw_hash = DatabaseHandler.getClient("TestClient1")?.password_hash + + client.post("/exit") { + contentType(ContentType.Application.Json) + setBody(ExitRequest(UUID.fromString("00000000-0000-0000-0000-000000000001"), Base64.getEncoder().encodeToString(pw_hash))) + }.apply { + assertEquals("Client removed", body()) + } + } + + @Test + fun testPostExitNoBody() = testApplication { + application { + module() + } + + client.post("/exit").apply { + assertEquals(400, this.status.value) + } + } +} \ No newline at end of file diff --git a/server/src/test/kotlin/org/muellerssoftware/openproximitychat/DatabaseHandlerTest.kt b/server/src/test/kotlin/org/muellerssoftware/openproximitychat/DatabaseHandlerTest.kt new file mode 100644 index 0000000..ef91d03 --- /dev/null +++ b/server/src/test/kotlin/org/muellerssoftware/openproximitychat/DatabaseHandlerTest.kt @@ -0,0 +1,85 @@ +package org.muellerssoftware.openproximitychat + +import org.junit.Before +import java.util.* +import kotlin.test.Test + +internal class DatabaseHandlerTest { + @Before + fun setUp() { + DatabaseHandler.clear() + DatabaseHandler.addClient( + UUID.fromString("00000000-0000-0000-0000-000000000001"), + "TestClient1", + "0.0.0.0:1", + ) + DatabaseHandler.addClient( + UUID.fromString("00000000-0000-0000-0000-000000000002"), + "TestClient2", + "0.0.0.0:1" + ) + DatabaseHandler.addClient( + UUID.fromString("00000000-0000-0000-0000-000000000003"), + "TestClient3", + "0.0.0.0:1" + ) + } + + @Test + fun addClient() { + DatabaseHandler.addClient(UUID.fromString("00000000-0000-0000-0000-000000000010"), "AddTestClient1", "0.0.0.0:1") + DatabaseHandler.getClient("AddTestClient1")?.let { + assert(it.name == "AddTestClient1") + assert(it.ip == "0.0.0.0:1") + assert(it.uuid == UUID.fromString("00000000-0000-0000-0000-000000000010")) + } + } + + @Test + fun getClient() { + DatabaseHandler.getClient("TestClient1")?.let { + assert(it.name == "TestClient1") + assert(it.ip == "0.0.0.0:1") + assert(it.uuid == UUID.fromString("00000000-0000-0000-0000-000000000001")) + } + + DatabaseHandler.getClient("TestClient2")?.let { + assert(it.name == "TestClient2") + assert(it.ip == "0.0.0.0:1") + assert(it.uuid == UUID.fromString("00000000-0000-0000-0000-000000000002")) + } + + DatabaseHandler.getClient("TestClient3")?.let { + assert(it.name == "TestClient3") + assert(it.ip == "0.0.0.0:1") + assert(it.uuid == UUID.fromString("00000000-0000-0000-0000-000000000003")) + } + } + + @Test + fun removeClient() { + DatabaseHandler.removeClient(UUID.fromString("00000000-0000-0000-0000-000000000001")) + assert(DatabaseHandler.getClient("TestClient1") == null) + + DatabaseHandler.removeClient(UUID.fromString("00000000-0000-0000-0000-000000000002")) + assert(DatabaseHandler.getClient("TestClient2") == null) + + DatabaseHandler.removeClient(UUID.fromString("00000000-0000-0000-0000-000000000003")) + assert(DatabaseHandler.getClient("TestClient3") == null) + } + + @Test + fun checkAuth() { + DatabaseHandler.getClient("TestClient1")?.let { + assert(DatabaseHandler.checkCredentials(it.uuid, it.password_hash)) + } + + DatabaseHandler.getClient("TestClient2")?.let { + assert(DatabaseHandler.checkCredentials(it.uuid, it.password_hash)) + } + + DatabaseHandler.getClient("TestClient3")?.let { + assert(DatabaseHandler.checkCredentials(it.uuid, it.password_hash)) + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4e90983..64d9bdc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,6 @@ rootProject.name = "OpenProximityChat" -include("forge", "fabric", "common") +include("forge", "fabric", "server", "common") pluginManagement { repositories {