diff --git a/sp-relay/SharedWorldRelay/.gitignore b/sp-relay/SharedWorldRelay/.gitignore new file mode 100644 index 0000000..341c9c0 --- /dev/null +++ b/sp-relay/SharedWorldRelay/.gitignore @@ -0,0 +1,11 @@ +lib/* +.idea/* +*.iml +out/* +deps/BungeeCord.jar +/.gradle/ +/.settings/ +.classpath +.project +/build/ +/bin/ diff --git a/sp-relay/SharedWorldRelay/build.gradle b/sp-relay/SharedWorldRelay/build.gradle new file mode 100644 index 0000000..79bf113 --- /dev/null +++ b/sp-relay/SharedWorldRelay/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'java' + id 'eclipse' +} + +group = 'net.lax1dude.eaglercraft.v1_8.sp.relay.server' +version = '' + +repositories { + mavenCentral() +} + +sourceSets { + main { + java { + srcDirs 'src/main/java' + srcDirs '../../sources/protocol-relay/java' + } + } +} + +dependencies { + implementation 'org.java-websocket:Java-WebSocket:1.5.6' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +jar { + compileJava.options.encoding = 'UTF-8' + javadoc.options.encoding = 'UTF-8' + + manifest { + attributes( + 'Main-Class': 'net.lax1dude.eaglercraft.v1_8.sp.relay.server.EaglerSPRelay' + ) + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + exclude 'META-INF/versions/9/module-info.class' +} diff --git a/sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.jar b/sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.jar differ diff --git a/sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.properties b/sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..eb7f6f9 --- /dev/null +++ b/sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jun 20 10:14:47 CDT 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/sp-relay/SharedWorldRelay/gradlew b/sp-relay/SharedWorldRelay/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/sp-relay/SharedWorldRelay/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/sp-relay/SharedWorldRelay/gradlew.bat b/sp-relay/SharedWorldRelay/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/sp-relay/SharedWorldRelay/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/sp-relay/SharedWorldRelay/settings.gradle b/sp-relay/SharedWorldRelay/settings.gradle new file mode 100644 index 0000000..5179352 --- /dev/null +++ b/sp-relay/SharedWorldRelay/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'SharedWorldRelay' + diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/ByteBufferInputStream.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/ByteBufferInputStream.java new file mode 100644 index 0000000..54c60ca --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/ByteBufferInputStream.java @@ -0,0 +1,78 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Copyright (c) 2022 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class ByteBufferInputStream extends InputStream { + + private final ByteBuffer buffer; + + public ByteBufferInputStream(ByteBuffer buf) { + buffer = buf; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int max = buffer.remaining(); + if(len > max) { + len = max; + } + buffer.get(b, off, len); + return len; + } + + @Override + public int read() throws IOException { + if(buffer.remaining() == 0) { + return -1; + }else { + return (int)buffer.get() & 0xFF; + } + } + + @Override + public long skip(long n) throws IOException { + int max = buffer.remaining(); + if(n > max) { + n = (int)max; + } + return max; + } + + @Override + public int available() throws IOException { + return buffer.remaining(); + } + + @Override + public synchronized void mark(int readlimit) { + buffer.mark(); + } + + @Override + public synchronized void reset() throws IOException { + buffer.reset(); + } + + @Override + public boolean markSupported() { + return true; + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/Constants.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/Constants.java new file mode 100644 index 0000000..dc7e81a --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/Constants.java @@ -0,0 +1,24 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +/** + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class Constants { + + public static final String versionName = "0.2a"; + public static final String versionBrand = "lax1dude"; + public static final int protocolVersion = 1; + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/DebugLogger.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/DebugLogger.java new file mode 100644 index 0000000..68419d0 --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/DebugLogger.java @@ -0,0 +1,267 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.IRelayLogger; + +/** + * Copyright (c) 2022 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class DebugLogger implements IRelayLogger { + + private static Level debugLoggingLevel = Level.INFO; + + public static void enableDebugLogging(Level level) { + if(level == null) { + level = Level.NONE; + } + debugLoggingLevel = level; + } + + public static boolean debugLoggingEnabled() { + return debugLoggingLevel != Level.NONE; + } + + private static final Map loggers = new HashMap(); + + public static DebugLogger getLogger(String name) { + DebugLogger ret = loggers.get(name); + if(ret == null) { + ret = new DebugLogger(name); + loggers.put(name, ret); + } + return ret; + } + + private final String name; + + private DebugLogger(String name) { + this.name = name; + } + + public static enum Level { + + NONE("NONE", 0, System.out), DEBUG("DEBUG", 4, System.out), INFO("INFO", 3, System.out), + WARN("WARN", 2, System.err), ERROR("ERROR", 1, System.err); + + public final String label; + public final int level; + public final PrintStream output; + + private Level(String label, int level, PrintStream output) { + this.label = label; + this.level = level; + this.output = output; + } + + } + + private class LogStream extends OutputStream { + + private final Level logLevel; + private final ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream(); + + private LogStream(Level logLevel) { + this.logLevel = logLevel; + } + + @Override + public void write(int b) throws IOException { + if(b == (int)'\r') { + return; + }else if(b == (int)'\n') { + byte[] line = lineBuffer.toByteArray(); + lineBuffer.reset(); + log(logLevel, new String(line, StandardCharsets.UTF_8)); + }else { + lineBuffer.write(b); + } + } + + } + + private OutputStream infoOutputStream = null; + private PrintStream infoPrintStream = null; + + private OutputStream warnOutputStream = null; + private PrintStream warnPrintStream = null; + + private OutputStream errorOutputStream = null; + private PrintStream errorPrintStream = null; + + public OutputStream getOutputStream(Level lvl) { + switch(lvl) { + case INFO: + default: + if(infoOutputStream == null) { + infoOutputStream = new LogStream(Level.INFO); + } + return infoOutputStream; + case WARN: + if(warnOutputStream == null) { + warnOutputStream = new LogStream(Level.WARN); + } + return warnOutputStream; + case ERROR: + if(errorOutputStream == null) { + errorOutputStream = new LogStream(Level.ERROR); + } + return errorOutputStream; + } + } + + public PrintStream getPrintStream(Level lvl) { + switch(lvl) { + case INFO: + default: + if(infoPrintStream == null) { + infoPrintStream = new PrintStream(getOutputStream(Level.INFO)); + } + return infoPrintStream; + case WARN: + if(warnPrintStream == null) { + warnPrintStream = new PrintStream(getOutputStream(Level.WARN)); + } + return warnPrintStream; + case ERROR: + if(errorPrintStream == null) { + errorPrintStream = new PrintStream(getOutputStream(Level.ERROR)); + } + return errorPrintStream; + } + } + + private static final SimpleDateFormat fmt = new SimpleDateFormat("hh:mm:ss+SSS"); + private final Date dateInstance = new Date(); + + public static String formatParams(String msg, Object... params) { + if(params.length > 0) { + StringBuilder builtString = new StringBuilder(); + for(int i = 0; i < params.length; ++i) { + int idx = msg.indexOf("{}"); + if(idx != -1) { + builtString.append(msg.substring(0, idx)); + if(params[i] instanceof InetSocketAddress) { + params[i] = Util.sock2String((InetSocketAddress)params[i]); + } + builtString.append(params[i]); + msg = msg.substring(idx + 2); + }else { + break; + } + } + builtString.append(msg); + return builtString.toString(); + }else { + return msg; + } + } + + public void log(Level lvl, String msg, Object... params) { + if(debugLoggingLevel.level >= lvl.level) { + synchronized(this) { + dateInstance.setTime(System.currentTimeMillis()); + System.out.println("[" + fmt.format(dateInstance) + "][" + Thread.currentThread().getName() + "/" + lvl.label + "][" + name + "]: " + + (params.length == 0 ? msg : formatParams(msg, params))); + } + } + } + + public void log(Level lvl, Throwable stackTrace) { + stackTrace.printStackTrace(getPrintStream(lvl)); + } + + public void debug(String msg) { + if(debugLoggingLevel.level >= Level.DEBUG.level) { + log(Level.DEBUG, msg); + } + } + + public void debug(String msg, Object... params) { + if(debugLoggingLevel.level >= Level.DEBUG.level) { + log(Level.DEBUG, msg, params); + } + } + + public void debug(Throwable t) { + if(debugLoggingLevel.level >= Level.DEBUG.level) { + log(Level.DEBUG, t); + } + } + + public void info(String msg) { + if(debugLoggingLevel.level >= Level.INFO.level) { + log(Level.INFO, msg); + } + } + + public void info(String msg, Object... params) { + if(debugLoggingLevel.level >= Level.INFO.level) { + log(Level.INFO, msg, params); + } + } + + public void info(Throwable t) { + if(debugLoggingLevel.level >= Level.INFO.level) { + log(Level.INFO, t); + } + } + + public void warn(String msg) { + if(debugLoggingLevel.level >= Level.WARN.level) { + log(Level.WARN, msg); + } + } + + public void warn(String msg, Object... params) { + if(debugLoggingLevel.level >= Level.WARN.level) { + log(Level.WARN, msg, params); + } + } + + public void warn(Throwable t) { + if(debugLoggingLevel.level >= Level.WARN.level) { + log(Level.WARN, t); + } + } + + public void error(String msg) { + if(debugLoggingLevel.level >= Level.ERROR.level) { + log(Level.ERROR, msg); + } + } + + public void error(String msg, Object... params) { + if(debugLoggingLevel.level >= Level.ERROR.level) { + log(Level.ERROR, msg, params); + } + } + + public void error(Throwable t) { + if(debugLoggingLevel.level >= Level.ERROR.level) { + log(Level.ERROR, t); + } + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPClient.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPClient.java new file mode 100644 index 0000000..51aa590 --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPClient.java @@ -0,0 +1,131 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.io.IOException; +import java.util.Random; + +import org.java_websocket.WebSocket; + +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.*; + +/** + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class EaglerSPClient { + + public final WebSocket socket; + public final EaglerSPServer server; + public final String id; + public final long createdOn; + public boolean serverNotifiedOfClose = false; + public LoginState state = LoginState.INIT; + public final String address; + + EaglerSPClient(WebSocket sock, EaglerSPServer srv, String id, String addr) { + this.socket = sock; + this.server = srv; + this.id = id; + this.createdOn = System.currentTimeMillis(); + this.address = addr; + } + + public void send(RelayPacket packet) { + if(this.socket.isOpen()) { + try { + this.socket.send(RelayPacket.writePacket(packet, EaglerSPRelay.logger)); + }catch(IOException ex) { + EaglerSPRelay.logger.debug("Error sending data to {}", (String) this.socket.getAttachment()); + EaglerSPRelay.logger.debug(ex); + disconnect(RelayPacketFEDisconnectClient.TYPE_INTERNAL_ERROR, "Internal Server Error"); + this.socket.close(); + } + }else { + EaglerSPRelay.logger.debug("WARNING: Tried to send data to {} after the connection closed.", (String) this.socket.getAttachment()); + } + } + + public boolean handle(RelayPacket packet) throws IOException { + if(packet instanceof RelayPacket03ICECandidate) { + if(LoginState.assertEquals(this, LoginState.RECIEVED_DESCRIPTION)) { + state = LoginState.SENT_ICE_CANDIDATE; + server.handleClientICECandidate(this, (RelayPacket03ICECandidate)packet); + EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x03: ICECandidate", (String) socket.getAttachment()); + } + return true; + }else if(packet instanceof RelayPacket04Description) { + if(LoginState.assertEquals(this, LoginState.INIT)) { + state = LoginState.SENT_DESCRIPTION; + server.handleClientDescription(this, (RelayPacket04Description)packet); + EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x04: Description", (String) socket.getAttachment()); + } + return true; + }else if(packet instanceof RelayPacket05ClientSuccess) { + if(LoginState.assertEquals(this, LoginState.RECIEVED_ICE_CANIDATE)) { + state = LoginState.FINISHED; + server.handleClientSuccess(this, (RelayPacket05ClientSuccess)packet); + EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x05: ClientSuccess", (String) socket.getAttachment()); + disconnect(RelayPacketFEDisconnectClient.TYPE_FINISHED_SUCCESS, "Successful connection"); + } + return true; + }else if(packet instanceof RelayPacket06ClientFailure) { + if(LoginState.assertEquals(this, LoginState.RECIEVED_ICE_CANIDATE)) { + state = LoginState.FINISHED; + server.handleClientFailure(this, (RelayPacket06ClientFailure)packet); + EaglerSPRelay.logger.debug("[{}][Client -> Relay -> Server] PKT 0x05: ClientFailure", (String) socket.getAttachment()); + disconnect(RelayPacketFEDisconnectClient.TYPE_FINISHED_FAILED, "Failed connection"); + } + return true; + }else { + return false; + } + } + + public void handleServerICECandidate(RelayPacket03ICECandidate desc) { + send(new RelayPacket03ICECandidate("", desc.candidate)); + } + + public void handleServerDescription(RelayPacket04Description desc) { + send(new RelayPacket04Description("", desc.description)); + } + + public void handleServerDisconnectClient(RelayPacketFEDisconnectClient packet) { + disconnect(packet.code, packet.reason); + } + + public void disconnect(int code, String reason) { + RelayPacket pkt = new RelayPacketFEDisconnectClient(id, code, reason); + if(!serverNotifiedOfClose) { + if (code != RelayPacketFEDisconnectClient.TYPE_FINISHED_SUCCESS) server.send(pkt); + serverNotifiedOfClose = true; + } + if(this.socket.isOpen()) { + send(pkt); + socket.close(); + } + EaglerSPRelay.logger.debug("[{}][Relay -> Client] PKT 0xFE: #{} {}", (String) socket.getAttachment(), code, reason); + } + + public static final int clientCodeLength = 16; + private static final String clientCodeChars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + public static String generateClientId() { + Random r = new Random(); + char[] ret = new char[clientCodeLength]; + for(int i = 0; i < ret.length; ++i) { + ret[i] = clientCodeChars.charAt(r.nextInt(clientCodeChars.length())); + } + return new String(ret); + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelay.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelay.java new file mode 100644 index 0000000..42fdc5b --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelay.java @@ -0,0 +1,528 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; + +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.*; +import net.lax1dude.eaglercraft.v1_8.sp.relay.server.RateLimiter.RateLimit; + +/** + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class EaglerSPRelay extends WebSocketServer { + + public static EaglerSPRelay instance; + public static final EaglerSPRelayConfig config = new EaglerSPRelayConfig(); + + private static RateLimiter pingRateLimiter = null; + private static RateLimiter worldRateLimiter = null; + + public static final DebugLogger logger = DebugLogger.getLogger("EaglerSPRelay"); + + public static void main(String[] args) throws IOException, InterruptedException { + for(int i = 0; i < args.length; ++i) { + if(args[i].equalsIgnoreCase("--debug")) { + DebugLogger.enableDebugLogging(DebugLogger.Level.DEBUG); + logger.debug("Debug logging enabled"); + } + } + + logger.info("Starting EaglerSPRelay version {}...", Constants.versionName); + config.load(new File("relayConfig.ini")); + + if(config.isPingRateLimitEnable()) { + pingRateLimiter = new RateLimiter(config.getPingRateLimitPeriod() * 1000, + config.getPingRateLimitLimit(), config.getPingRateLimitLockoutLimit(), + config.getPingRateLimitLockoutDuration() * 1000); + } + + if(config.isWorldRateLimitEnable()) { + worldRateLimiter = new RateLimiter(config.getWorldRateLimitPeriod() * 1000, + config.getWorldRateLimitLimit(), config.getWorldRateLimitLockoutLimit(), + config.getWorldRateLimitLockoutDuration() * 1000); + } + + EaglerSPRelayConfigRelayList.loadRelays(new File("relays.txt")); + + logger.info("Starting WebSocket Server..."); + instance = new EaglerSPRelay(new InetSocketAddress(config.getAddress(), config.getPort())); + instance.setConnectionLostTimeout(20); + instance.setReuseAddr(true); + instance.start(); + + Thread tickThread = new Thread((() -> { + int rateLimitUpdateCounter = 0; + while(true) { + try { + long millis = System.currentTimeMillis(); + synchronized(pendingConnections) { + Iterator> itr = pendingConnections.entrySet().iterator(); + while(itr.hasNext()) { + Entry etr = itr.next(); + if(millis - etr.getValue().openTime > 500l) { + etr.getKey().close(); + itr.remove(); + } + } + } + synchronized(clientConnections) { + Iterator itr = clientConnections.values().iterator(); + while(itr.hasNext()) { + EaglerSPClient cl = itr.next(); + if(millis - cl.createdOn > 10000l) { + cl.disconnect(RelayPacketFEDisconnectClient.TYPE_TIMEOUT, "Took too long to connect!"); + } + } + } + if(++rateLimitUpdateCounter > 300) { + if(pingRateLimiter != null) { + pingRateLimiter.update(); + } + if(worldRateLimiter != null) { + worldRateLimiter.update(); + } + rateLimitUpdateCounter = 0; + } + }catch(Throwable t) { + logger.error("Error in update loop!"); + logger.error(t); + } + Util.sleep(100l); + } + }), "Relay Tick"); + tickThread.setDaemon(true); + tickThread.start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + String s; + while((s = reader.readLine()) != null) { + s = s.trim(); + if(s.equalsIgnoreCase("stop") || s.equalsIgnoreCase("end")) { + logger.info("Shutting down..."); + instance.stop(); + System.exit(0); + }else if(s.equalsIgnoreCase("reset")) { + logger.info("Clearing all ratelimits"); + if(pingRateLimiter != null) pingRateLimiter.reset(); + if(worldRateLimiter != null) worldRateLimiter.reset(); + }else { + logger.info("Unknown command: {}", s); + logger.info("Type 'stop' to exit" + ((worldRateLimiter != null || pingRateLimiter != null) ? ", 'reset' to clear ratelimits" : "")); + } + } + + } + + private EaglerSPRelay(InetSocketAddress addr) { + super(addr); + } + + private static class PendingConnection { + + private final long openTime; + private final String address; + + public PendingConnection(long openTime, String address) { + this.openTime = openTime; + this.address = address; + } + + } + + private static final Map pendingConnections = new HashMap(); + private static final Map clientIds = new HashMap(); + private static final Map clientConnections = new HashMap(); + private static final Map serverCodes = new HashMap(); + private static final Map serverConnections = new HashMap(); + private static final Map> clientAddressSets = new HashMap(); + private static final Map> serverAddressSets = new HashMap(); + + @Override + public void onStart() { + logger.info("Listening on {}", getAddress()); + logger.info("Type 'stop' to exit" + ((worldRateLimiter != null || pingRateLimiter != null) ? ", 'reset' to clear ratelimits" : "")); + } + + @Override + public void onOpen(WebSocket arg0, ClientHandshake arg1) { + if(!config.getIsWhitelisted(arg1.getFieldValue("origin"))) { + arg0.close(); + return; + } + + String addr; + long millis = System.currentTimeMillis(); + if(config.isEnableRealIpHeader() && arg1.hasFieldValue(config.getRealIPHeaderName())) { + addr = arg1.getFieldValue(config.getRealIPHeaderName()).toLowerCase(); + }else { + addr = arg0.getRemoteSocketAddress().getAddress().getHostAddress().toLowerCase(); + } + + int totalCons = 0; + synchronized(pendingConnections) { + Iterator pendingItr = pendingConnections.values().iterator(); + while(pendingItr.hasNext()) { + if(pendingItr.next().address.equals(addr)) { + ++totalCons; + } + } + } + synchronized(clientAddressSets) { + List lst = clientAddressSets.get(addr); + if(lst != null) { + totalCons += lst.size(); + } + } + + if(totalCons >= config.getConnectionsPerIP()) { + logger.debug("[{}]: Too many connections are open on this address", (String) arg0.getAttachment()); + arg0.send(RelayPacketFEDisconnectClient.ratelimitPacketTooMany); + arg0.close(); + return; + } + + arg0.setAttachment(addr); + + PendingConnection waiting = new PendingConnection(millis, addr); + logger.debug("[{}]: Connection opened", arg0.getRemoteSocketAddress()); + synchronized(pendingConnections) { + pendingConnections.put(arg0, waiting); + } + } + + @Override + public void onMessage(WebSocket arg0, ByteBuffer arg1) { + DataInputStream sid = new DataInputStream(new ByteBufferInputStream(arg1)); + PendingConnection waiting; + synchronized(pendingConnections) { + waiting = pendingConnections.remove(arg0); + } + try { + RelayPacket pkt = RelayPacket.readPacket(sid, EaglerSPRelay.logger); + if(waiting != null) { + if(pkt instanceof RelayPacket00Handshake) { + RelayPacket00Handshake ipkt = (RelayPacket00Handshake)pkt; + if(ipkt.connectionVersion != Constants.protocolVersion) { + logger.debug("[{}]: Connected with unsupported protocol version: {} (supported " + + "version: {})", (String) arg0.getAttachment(), ipkt.connectionVersion, Constants.protocolVersion); + if(ipkt.connectionVersion < Constants.protocolVersion) { + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_PROTOCOL_VERSION, + "Outdated Client! (v" + Constants.protocolVersion + " req)"), EaglerSPRelay.logger)); + }else { + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_PROTOCOL_VERSION, + "Outdated Server! (still on v" + Constants.protocolVersion + ")"), EaglerSPRelay.logger)); + } + arg0.close(); + return; + } + if(ipkt.connectionType == 0x01) { + if(!rateLimit(worldRateLimiter, arg0, waiting.address)) { + logger.debug("[{}]: Got world ratelimited", (String) arg0.getAttachment()); + return; + } + synchronized(serverAddressSets) { + List lst = serverAddressSets.get(waiting.address); + if(lst != null) { + if(lst.size() >= config.getWorldsPerIP()) { + logger.debug("[{}]: Too many worlds are open on this address", (String) arg0.getAttachment()); + arg0.send(RelayPacketFEDisconnectClient.ratelimitPacketTooMany); + arg0.close(); + return; + } + } + } + logger.debug("[{}]: Connected as a server", (String) arg0.getAttachment()); + EaglerSPServer srv; + synchronized(serverCodes) { + int j = 0; + String code; + do { + if(++j > 100) { + logger.error("Error: relay is running out of codes!"); + logger.error("Closing connection to {}", (String) arg0.getAttachment()); + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_INTERNAL_ERROR, + "Internal Server Error"), EaglerSPRelay.logger)); + arg0.close(); + return; + } + code = config.generateCode(); + }while(serverCodes.containsKey(code)); + srv = new EaglerSPServer(arg0, code, ipkt.connectionCode, waiting.address); + serverCodes.put(code, srv); + ipkt.connectionCode = code; + arg0.send(RelayPacket.writePacket(ipkt, EaglerSPRelay.logger)); + logger.debug("[{}][Relay -> Server] PKT 0x00: Assign join code: {}", (String) arg0.getAttachment(), code); + } + synchronized(serverConnections) { + serverConnections.put(arg0, srv); + } + synchronized(serverAddressSets) { + List lst = serverAddressSets.get(srv.serverAddress); + if(lst == null) { + lst = new ArrayList(); + serverAddressSets.put(srv.serverAddress, lst); + } + lst.add(srv); + } + srv.send(new RelayPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers)); + logger.debug("[{}][Relay -> Server] PKT 0x01: Send ICE server list to server", (String) arg0.getAttachment()); + }else { + if(!rateLimit(pingRateLimiter, arg0, waiting.address)) { + logger.debug("[{}]: Got ping ratelimited", (String) arg0.getAttachment()); + return; + } + if(ipkt.connectionType == 0x02) { + String code = ipkt.connectionCode; + logger.debug("[{}]: Connected as a client, requested server code: {}", (String) arg0.getAttachment(), code); + if(code.length() != config.getCodeLength()) { + logger.debug("The code '{}' is invalid because it's the wrong length, disconnecting", code); + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_CODE_LENGTH, + "The join code is the wrong length, it should be " + config.getCodeLength() + " chars long"), EaglerSPRelay.logger)); + arg0.close(); + }else { + if(!config.isCodeMixCase()) { + code = code.toLowerCase(); + } + EaglerSPServer srv; + synchronized(serverCodes) { + srv = serverCodes.get(code); + } + if(srv == null) { + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_INCORRECT_CODE, + "Invalid code, no LAN world found!"), EaglerSPRelay.logger)); + arg0.close(); + return; + } + String id; + EaglerSPClient cl; + synchronized(clientIds) { + int j = 0; + do { + id = EaglerSPClient.generateClientId(); + }while(clientIds.containsKey(id)); + cl = new EaglerSPClient(arg0, srv, id, waiting.address); + clientIds.put(id, cl); + ipkt.connectionCode = id; + arg0.send(RelayPacket.writePacket(ipkt, EaglerSPRelay.logger)); + srv.handleNewClient(cl); + } + synchronized(clientConnections) { + clientConnections.put(arg0, cl); + } + synchronized(clientAddressSets) { + List lst = clientAddressSets.get(cl.address); + if(lst == null) { + lst = new ArrayList(); + clientAddressSets.put(cl.address, lst); + } + lst.add(cl); + } + cl.send(new RelayPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers)); + logger.debug("[{}][Relay -> Client] PKT 0x01: Send ICE server list to client", (String) arg0.getAttachment()); + } + }else if(ipkt.connectionType == 0x03) { + logger.debug("[{}]: Pinging the server", (String) arg0.getAttachment()); + arg0.send(RelayPacket.writePacket(new RelayPacket69Pong(Constants.protocolVersion, config.getComment(), Constants.versionBrand), EaglerSPRelay.logger)); + arg0.close(); + }else if(ipkt.connectionType == 0x04) { + logger.debug("[{}]: Polling the server for other worlds", (String) arg0.getAttachment()); + if(config.isEnableShowLocals()) { + arg0.send(RelayPacket.writePacket(new RelayPacket07LocalWorlds(getLocalWorlds(waiting.address)), EaglerSPRelay.logger)); + }else { + arg0.send(RelayPacket.writePacket(new RelayPacket07LocalWorlds(null), EaglerSPRelay.logger)); + } + arg0.close(); + }else { + logger.debug("[{}]: Unknown connection type: {}", (String) arg0.getAttachment(), ipkt.connectionType); + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_ILLEGAL_OPERATION, + "Unexpected Init Packet"), EaglerSPRelay.logger)); + arg0.close(); + } + } + }else { + logger.debug("[{}]: Pending connection did not send a 0x00 packet to identify " + + "as a client or server", (String) arg0.getAttachment()); + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_ILLEGAL_OPERATION, + "Unexpected Init Packet"), EaglerSPRelay.logger)); + arg0.close(); + } + }else { + EaglerSPServer srv; + synchronized(serverConnections) { + srv = serverConnections.get(arg0); + } + if(srv != null) { + if(!srv.handle(pkt)) { + logger.debug("[{}]: Server sent invalid packet: {}", (String) arg0.getAttachment(), pkt.getClass().getSimpleName()); + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_INVALID_PACKET, + "Invalid Packet Recieved"), EaglerSPRelay.logger)); + arg0.close(); + } + }else { + EaglerSPClient cl; + synchronized(clientConnections) { + cl = clientConnections.get(arg0); + } + if(cl != null) { + if(!cl.handle(pkt)) { + logger.debug("[{}]: Client sent invalid packet: {}", (String) arg0.getAttachment(), pkt.getClass().getSimpleName()); + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_INVALID_PACKET, + "Invalid Packet Recieved"), EaglerSPRelay.logger)); + arg0.close(); + } + }else { + logger.debug("[{}]: Connection has no client/server attached to it!", (String) arg0.getAttachment()); + arg0.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_ILLEGAL_OPERATION, + "Internal Server Error"), EaglerSPRelay.logger)); + arg0.close(); + } + } + } + }catch(Throwable t) { + logger.error("[{}]: Failed to handle binary frame: {}", (String) arg0.getAttachment(), t); + arg0.close(); + } + } + + @Override + public void onMessage(WebSocket arg0, String arg1) { + logger.debug("[{}]: Sent a text frame, disconnecting", (String) arg0.getAttachment()); + arg0.close(); + } + + @Override + public void onClose(WebSocket arg0, int arg1, String arg2, boolean arg3) { + EaglerSPServer srv; + synchronized(serverConnections) { + srv = serverConnections.remove(arg0); + } + if(srv != null) { + logger.debug("[{}]: Server closed, code: {}", (String) arg0.getAttachment(), srv.code); + synchronized(serverCodes) { + serverCodes.remove(srv.code); + } + synchronized(serverAddressSets) { + List lst = serverAddressSets.get(srv.serverAddress); + if(lst != null) { + lst.remove(srv); + if(lst.size() == 0) { + serverAddressSets.remove(srv.serverAddress); + } + } + } + ArrayList clientList; + synchronized(clientConnections) { + clientList = new ArrayList(clientConnections.values()); + } + Iterator itr = clientList.iterator(); + while(itr.hasNext()) { + EaglerSPClient cl = itr.next(); + if(cl.server == srv) { + logger.debug("[{}]: Disconnecting client: {} (id: ", (String) cl.socket.getAttachment(), cl.id); + cl.socket.close(); + } + } + }else { + EaglerSPClient cl; + synchronized(clientConnections) { + cl = clientConnections.remove(arg0); + } + if(cl != null) { + synchronized(clientAddressSets) { + List lst = clientAddressSets.get(cl.address); + if(lst != null) { + lst.remove(cl); + if(lst.size() == 0) { + clientAddressSets.remove(cl.address); + } + } + } + logger.debug("[{}]: Client closed, id: {}", (String) arg0.getAttachment(), cl.id); + synchronized(clientIds) { + clientIds.remove(cl.id); + } + cl.server.handleClientDisconnect(cl); + }else { + logger.debug("[{}]: Connection Closed", (String) arg0.getAttachment()); + } + } + } + + @Override + public void onError(WebSocket arg0, Exception arg1) { + logger.error("[{}]: Exception thrown: {}", (arg0 == null ? "SERVER" : (String) arg0.getAttachment()), arg1.toString()); + logger.debug(arg1); + arg0.close(); + } + + private List getLocalWorlds(String addr) { + List lst = new ArrayList(); + synchronized(serverAddressSets) { + List srvs = serverAddressSets.get(addr); + if(srvs != null) { + if(srvs.size() == 0) { + serverAddressSets.remove(addr); + }else { + for(EaglerSPServer s : srvs) { + if(!s.serverHidden) { + lst.add(new RelayPacket07LocalWorlds.LocalWorld(s.serverName, s.code)); + } + } + } + } + } + return lst; + } + + private boolean rateLimit(RateLimiter limiter, WebSocket sock, String addr) { + if(limiter != null) { + RateLimit l = limiter.limit(addr); + if(l == RateLimit.NONE) { + return true; + }else if(l == RateLimit.LIMIT) { + sock.send(RelayPacketFEDisconnectClient.ratelimitPacketBlock); + sock.close(); + return false; + }else if(l == RateLimit.LIMIT_NOW_LOCKOUT) { + sock.send(RelayPacketFEDisconnectClient.ratelimitPacketBlockLock); + sock.close(); + return false; + }else if(l == RateLimit.LOCKOUT) { + sock.close(); + return false; + }else { + return true; // ? + } + }else { + return true; + } + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelayConfig.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelayConfig.java new file mode 100644 index 0000000..19912d0 --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelayConfig.java @@ -0,0 +1,495 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class EaglerSPRelayConfig { + + private String address = "0.0.0.0"; + private int port = 6699; + private int codeLength = 5; + private String codeChars = "abcdefghijklmnopqrstuvwxyz0123456789"; + private boolean codeMixCase = false; + + private int connectionsPerIP = 128; + private int worldsPerIP = 32; + + private boolean openRateLimitEnable = true; + private int openRateLimitPeriod = 192; + private int openRateLimitLimit = 32; + private int openRateLimitLockoutLimit = 48; + private int openRateLimitLockoutDuration = 600; + + private boolean pingRateLimitEnable = true; + private int pingRateLimitPeriod = 256; + private int pingRateLimitLimit = 128; + private int pingRateLimitLockoutLimit = 192; + private int pingRateLimitLockoutDuration = 300; + + private String originWhitelist = ""; + private String[] originWhitelistArray = new String[0]; + private boolean enableRealIpHeader = false; + private String realIpHeaderName = "X-Real-IP"; + private boolean enableShowLocals = true; + private String serverComment = "Eags. Public LAN Relay"; + + public void load(File conf) { + if(!conf.isFile()) { + EaglerSPRelay.logger.info("Creating config file: {}", conf.getAbsoluteFile()); + save(conf); + }else { + EaglerSPRelay.logger.info("Loading config file: {}", conf.getAbsoluteFile()); + boolean gotPort = false, gotCodeLength = false, gotCodeChars = false; + boolean gotCodeMixCase = false; + boolean gotConnectionsPerIP = false, gotWorldsPerIP = false, + gotOpenRateLimitEnable = false, gotOpenRateLimitPeriod = false, + gotOpenRateLimitLimit = false, gotOpenRateLimitLockoutLimit = false, + gotOpenRateLimitLockoutDuration = false; + boolean gotPingRateLimitEnable = false, gotPingRateLimitPeriod = false, + gotPingRateLimitLimit = false, gotPingRateLimitLockoutLimit = false, + gotPingRateLimitLockoutDuration = false; + boolean gotOriginWhitelist = false, gotEnableRealIpHeader = false, + gotRealIpHeaderName = false, gotAddress = false, gotComment = false, + gotShowLocals = false; + + Throwable t2 = null; + try(BufferedReader reader = new BufferedReader(new FileReader(conf))) { + String s; + while((s = reader.readLine()) != null) { + String[] ss = s.trim().split(":", 2); + if(ss.length == 2) { + ss[0] = ss[0].trim(); + ss[1] = ss[1].trim(); + if(ss[0].equalsIgnoreCase("port")) { + try { + port = Integer.parseInt(ss[1]); + gotPort = true; + }catch(Throwable t) { + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("address")) { + address = ss[1]; + gotAddress = true; + }else if(ss[0].equalsIgnoreCase("code-length")) { + try { + codeLength = Integer.parseInt(ss[1]); + gotCodeLength = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid code-length {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("code-chars")) { + if(ss[1].length() < 2) { + t2 = new IllegalArgumentException("not enough chars"); + EaglerSPRelay.logger.warn("Invalid code-chars {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t2); + }else { + codeChars = ss[1]; + gotCodeChars = true; + } + }else if(ss[0].equalsIgnoreCase("code-mix-case")) { + try { + codeMixCase = getBooleanValue(ss[1]); + gotCodeMixCase = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid code-mix-case {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("worlds-per-ip")) { + try { + worldsPerIP = Integer.parseInt(ss[1]); + gotWorldsPerIP = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid worlds-per-ip {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-enable")) { + try { + openRateLimitEnable = getBooleanValue(ss[1]); + gotOpenRateLimitEnable = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-enable {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-period")) { + try { + openRateLimitPeriod = Integer.parseInt(ss[1]); + gotOpenRateLimitPeriod = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-limit")) { + try { + openRateLimitLimit = Integer.parseInt(ss[1]); + gotOpenRateLimitLimit = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-lockout-limit")) { + try { + openRateLimitLockoutLimit = Integer.parseInt(ss[1]); + gotOpenRateLimitLockoutLimit = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-lockout-duration")) { + try { + openRateLimitLockoutDuration = Integer.parseInt(ss[1]); + gotOpenRateLimitLockoutDuration = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("connections-per-ip")) { + try { + connectionsPerIP = Integer.parseInt(ss[1]); + gotConnectionsPerIP = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid connections-per-ip {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-enable")) { + try { + pingRateLimitEnable = getBooleanValue(ss[1]); + gotPingRateLimitEnable = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-enable {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-period")) { + try { + pingRateLimitPeriod = Integer.parseInt(ss[1]); + gotPingRateLimitPeriod = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-limit")) { + try { + pingRateLimitLimit = Integer.parseInt(ss[1]); + gotPingRateLimitLimit = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-lockout-limit")) { + try { + pingRateLimitLockoutLimit = Integer.parseInt(ss[1]); + gotPingRateLimitLockoutLimit = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-lockout-duration")) { + try { + pingRateLimitLockoutDuration = Integer.parseInt(ss[1]); + gotPingRateLimitLockoutDuration = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("origin-whitelist")) { + originWhitelist = ss[1]; + gotOriginWhitelist = true; + }else if(ss[0].equalsIgnoreCase("enable-real-ip-header")) { + try { + enableRealIpHeader = getBooleanValue(ss[1]); + gotEnableRealIpHeader = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid enable-real-ip-header {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("real-ip-header-name")) { + realIpHeaderName = ss[1]; + gotRealIpHeaderName = true; + }else if(ss[0].equalsIgnoreCase("show-local-worlds")) { + try { + enableShowLocals = getBooleanValue(ss[1]); + gotShowLocals = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid show-local-worlds {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("server-comment")) { + serverComment = ss[1]; + gotComment = true; + } + } + } + }catch(IOException t) { + EaglerSPRelay.logger.error("Failed to load config file: {}", conf.getAbsoluteFile()); + EaglerSPRelay.logger.error(t); + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid config file: {}", conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + } + if(t2 != null || !gotPort || !gotCodeLength || !gotCodeChars || + !gotCodeMixCase || !gotWorldsPerIP || !gotOpenRateLimitEnable || + !gotOpenRateLimitPeriod || !gotOpenRateLimitLimit || + !gotOpenRateLimitLockoutLimit || !gotOpenRateLimitLockoutDuration || + !gotConnectionsPerIP || !gotPingRateLimitEnable || + !gotPingRateLimitPeriod || !gotPingRateLimitLimit || + !gotPingRateLimitLockoutLimit || !gotPingRateLimitLockoutDuration || + !gotOriginWhitelist || !gotEnableRealIpHeader || !gotAddress || + !gotComment || !gotShowLocals || !gotRealIpHeaderName) { + EaglerSPRelay.logger.warn("Updating config file: {}", conf.getAbsoluteFile()); + save(conf); + } + String[] splitted = originWhitelist.split(";"); + List splittedList = new ArrayList(); + for(int i = 0; i < splitted.length; ++i) { + splitted[i] = splitted[i].trim().toLowerCase(); + if(splitted[i].length() > 0) { + splittedList.add(splitted[i]); + } + } + originWhitelistArray = new String[splittedList.size()]; + for(int i = 0; i < originWhitelistArray.length; ++i) { + originWhitelistArray[i] = splittedList.get(i); + } + } + } + + public void save(File conf) { + try(PrintWriter w = new PrintWriter(new FileOutputStream(conf))) { + w.println("[EaglerSPRelay]"); + w.println("address: " + address); + w.println("port: " + port); + w.println("code-length: " + codeLength); + w.println("code-chars: " + codeChars); + w.println("code-mix-case: " + codeMixCase); + w.println("connections-per-ip: " + connectionsPerIP); + w.println("ping-ratelimit-enable: " + pingRateLimitEnable); + w.println("ping-ratelimit-period: " + pingRateLimitPeriod); + w.println("ping-ratelimit-limit: " + pingRateLimitLimit); + w.println("ping-ratelimit-lockout-limit: " + pingRateLimitLockoutLimit); + w.println("ping-ratelimit-lockout-duration: " + pingRateLimitLockoutDuration); + w.println("worlds-per-ip: " + worldsPerIP); + w.println("world-ratelimit-enable: " + openRateLimitEnable); + w.println("world-ratelimit-period: " + openRateLimitPeriod); + w.println("world-ratelimit-limit: " + openRateLimitLimit); + w.println("world-ratelimit-lockout-limit: " + openRateLimitLockoutLimit); + w.println("world-ratelimit-lockout-duration: " + openRateLimitLockoutDuration); + w.println("origin-whitelist: " + originWhitelist); + w.println("real-ip-header-name: " + realIpHeaderName); + w.println("enable-real-ip-header: " + enableRealIpHeader); + w.println("show-local-worlds: " + isEnableShowLocals()); + w.print("server-comment: " + serverComment); + }catch(IOException t) { + EaglerSPRelay.logger.error("Failed to write config file: {}", conf.getAbsoluteFile()); + EaglerSPRelay.logger.error(t); + } + } + + private static boolean getBooleanValue(String str) { + if(str.equalsIgnoreCase("true") || str.equals("1")) { + return true; + }else if(str.equalsIgnoreCase("false") || str.equals("0")) { + return false; + }else { + throw new IllegalArgumentException("Not a boolean: " + str); + } + } + + public String getAddress() { + return address; + } + + public int getPort() { + return port; + } + + public int getCodeLength() { + return codeLength; + } + + public String getCodeChars() { + return codeChars; + } + + public boolean isCodeMixCase() { + return codeMixCase; + } + + public int getConnectionsPerIP() { + return connectionsPerIP; + } + + public boolean isPingRateLimitEnable() { + return pingRateLimitEnable; + } + + public int getPingRateLimitPeriod() { + return pingRateLimitPeriod; + } + + public int getPingRateLimitLimit() { + return pingRateLimitLimit; + } + + public int getPingRateLimitLockoutLimit() { + return pingRateLimitLockoutLimit; + } + + public int getPingRateLimitLockoutDuration() { + return pingRateLimitLockoutDuration; + } + + public int getWorldsPerIP() { + return worldsPerIP; + } + + public boolean isWorldRateLimitEnable() { + return openRateLimitEnable; + } + + public int getWorldRateLimitPeriod() { + return openRateLimitPeriod; + } + + public int getWorldRateLimitLimit() { + return openRateLimitLimit; + } + + public int getWorldRateLimitLockoutLimit() { + return openRateLimitLockoutLimit; + } + + public int getWorldRateLimitLockoutDuration() { + return openRateLimitLockoutDuration; + } + + public String getOriginWhitelist() { + return originWhitelist; + } + + public String[] getOriginWhitelistArray() { + return originWhitelistArray; + } + + public boolean getIsWhitelisted(String domain) { + if(originWhitelistArray.length == 0) { + return true; + }else { + if(domain == null) { + domain = "null"; + }else { + domain = domain.toLowerCase(); + if(domain.equals("null")) { + domain = "offline"; + }else { + if(domain.startsWith("http://")) { + domain = domain.substring(7); + }else if(domain.startsWith("https://")) { + domain = domain.substring(8); + } + } + } + for(int i = 0; i < originWhitelistArray.length; ++i) { + String etr = originWhitelistArray[i].toLowerCase(); + if(etr.startsWith("*")) { + if(domain.endsWith(etr.substring(1))) { + return true; + } + }else { + if(domain.equals(etr)) { + return true; + } + } + } + return false; + } + } + + public String getRealIPHeaderName() { + return realIpHeaderName; + } + + public boolean isEnableRealIpHeader() { + return enableRealIpHeader; + } + + public String getComment() { + return serverComment; + } + + public String generateCode() { + Random r = new Random(); + char[] ret = new char[codeLength]; + for(int i = 0; i < codeLength; ++i) { + ret[i] = codeChars.charAt(r.nextInt(codeChars.length())); + if(codeMixCase) { + if(r.nextBoolean()) { + ret[i] = Character.toLowerCase(ret[i]); + }else { + ret[i] = Character.toUpperCase(ret[i]); + } + } + } + return new String(ret); + } + + public boolean isEnableShowLocals() { + return enableShowLocals; + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelayConfigRelayList.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelayConfigRelayList.java new file mode 100644 index 0000000..59432e0 --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPRelayConfigRelayList.java @@ -0,0 +1,140 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; + +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacket01ICEServers; + +/** + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class EaglerSPRelayConfigRelayList { + + public static final Collection relayServers = new ArrayList(); + + public static void loadRelays(File list) throws IOException { + ArrayList loading = new ArrayList(); + + if(!list.isFile()) { + EaglerSPRelay.logger.info("Creating new {}...", list.getName()); + try(InputStream is = EaglerSPRelayConfigRelayList.class.getResourceAsStream("/relays.txt"); + FileOutputStream os = new FileOutputStream(list)) { + byte[] buffer = new byte[4096]; + int i; + while((i = is.read(buffer)) != -1) { + os.write(buffer, 0, i); + } + } + } + + EaglerSPRelay.logger.info("Loading STUN/TURN relays from: {}", list.getName()); + + RelayPacket01ICEServers.RelayType addType = null; + String addAddress = null; + String addUsername = null; + String addPassword = null; + try(BufferedReader reader = new BufferedReader(new FileReader(list))) { + String line; + while((line = reader.readLine()) != null) { + line = line.trim(); + if(line.length() == 0) { + continue; + } + boolean isSTUNHead = line.equals("[STUN]"); + boolean isTURNHead = line.equals("[TURN]"); + if(isSTUNHead || isTURNHead) { + if(addType != null) { + add(list.getName(), loading, addType, addAddress, addUsername, addPassword); + } + addAddress = null; + addUsername = null; + addPassword = null; + addType = null; + } + if(isSTUNHead) { + addType = RelayPacket01ICEServers.RelayType.NO_PASSWD; + }else if(isTURNHead) { + addType = RelayPacket01ICEServers.RelayType.PASSWD; + }else if(line.startsWith("url")) { + int spidx = line.indexOf('=') + 1; + if(spidx < 3) { + EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line); + }else { + line = line.substring(spidx).trim(); + if(line.length() < 1) { + EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line); + }else { + addAddress = line; + } + } + }else if(line.startsWith("username")) { + int spidx = line.indexOf('=') + 1; + if(spidx < 8) { + EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line); + }else { + line = line.substring(spidx).trim(); + if(line.length() < 1) { + EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line); + }else { + addUsername = line; + } + } + }else if(line.startsWith("password")) { + int spidx = line.indexOf('=') + 1; + if(spidx < 8) { + EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line); + }else { + line = line.substring(spidx).trim(); + if(line.length() < 1) { + EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line); + }else { + addPassword = line; + } + } + }else { + EaglerSPRelay.logger.error("Error: Invalid line in {}: ", line); + } + } + } + + if(addType != null) { + add(list.getName(), loading, addType, addAddress, addUsername, addPassword); + } + + if(loading.size() == 0) { + throw new IOException(list.getName() + ": no servers loaded"); + }else { + relayServers.clear(); + relayServers.addAll(loading); + } + + } + + private static void add(String filename, Collection loading, + RelayPacket01ICEServers.RelayType type, String url, String user, String pass) { + if(url == null) { + EaglerSPRelay.logger.error("Error: Invalid relay in {}, missing 'url'", filename); + }else { + loading.add(new RelayPacket01ICEServers.RelayServer(url, type, user, pass)); + } + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPServer.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPServer.java new file mode 100644 index 0000000..7e7686c --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/EaglerSPServer.java @@ -0,0 +1,153 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.java_websocket.WebSocket; + +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.*; + +/** + * Copyright (c) 2022-2024 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class EaglerSPServer { + + public final WebSocket socket; + public final String code; + public final Map clients; + public final String serverName; + public final String serverAddress; + public final boolean serverHidden; + + EaglerSPServer(WebSocket sock, String code, String serverName, String serverAddress) { + this.socket = sock; + this.code = code; + this.clients = new HashMap(); + + if(serverName.endsWith(";1")) { + this.serverHidden = true; + serverName = serverName.substring(0, serverName.length() - 2); + }else if(serverName.endsWith(";0")) { + this.serverHidden = false; + serverName = serverName.substring(0, serverName.length() - 2); + }else { + this.serverHidden = false; + } + + this.serverName = serverName; + this.serverAddress = serverAddress; + } + + public void send(RelayPacket packet) { + if(this.socket.isOpen()) { + try { + this.socket.send(RelayPacket.writePacket(packet, EaglerSPRelay.logger)); + }catch(IOException ex) { + EaglerSPRelay.logger.debug("Error sending data to {}", this.serverAddress); + EaglerSPRelay.logger.debug(ex); + try { + this.socket.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_INTERNAL_ERROR, + "Internal Server Error"), EaglerSPRelay.logger)); + }catch(IOException ex2) { + } + this.socket.close(); + } + }else { + EaglerSPRelay.logger.debug("WARNING: Tried to send data to {} after the connection closed.", this.serverAddress); + } + } + + public boolean handle(RelayPacket _packet) throws IOException { + if(_packet instanceof RelayPacket03ICECandidate) { + RelayPacket03ICECandidate packet = (RelayPacket03ICECandidate)_packet; + EaglerSPClient cl = clients.get(packet.peerId); + if(cl != null) { + if(LoginState.assertEquals(cl, LoginState.SENT_ICE_CANDIDATE)) { + cl.state = LoginState.RECIEVED_ICE_CANIDATE; + cl.handleServerICECandidate(packet); + EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0x03: ICECandidate", (String) cl.socket.getAttachment()); + } + }else { + this.socket.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_UNKNOWN_CLIENT, + "Unknown Client ID: " + packet.peerId), EaglerSPRelay.logger)); + } + return true; + }else if(_packet instanceof RelayPacket04Description) { + RelayPacket04Description packet = (RelayPacket04Description)_packet; + EaglerSPClient cl = clients.get(packet.peerId); + if(cl != null) { + if(LoginState.assertEquals(cl, LoginState.SENT_DESCRIPTION)) { + cl.state = LoginState.RECIEVED_DESCRIPTION; + cl.handleServerDescription(packet); + EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0x04: Description", (String) cl.socket.getAttachment()); + } + }else { + this.socket.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_UNKNOWN_CLIENT, + "Unknown Client ID: " + packet.peerId), EaglerSPRelay.logger)); + } + return true; + }else if(_packet instanceof RelayPacketFEDisconnectClient) { + RelayPacketFEDisconnectClient packet = (RelayPacketFEDisconnectClient)_packet; + EaglerSPClient cl = clients.get(packet.clientId); + if(cl != null) { + cl.handleServerDisconnectClient(packet); + EaglerSPRelay.logger.debug("[{}][Server -> Relay -> Client] PKT 0xFE: Disconnect: {}: {}", (String) cl.socket.getAttachment(), + packet.code, packet.reason); + }else { + this.socket.send(RelayPacket.writePacket(new RelayPacketFFErrorCode(RelayPacketFFErrorCode.TYPE_UNKNOWN_CLIENT, + "Unknown Client ID: " + packet.clientId), EaglerSPRelay.logger)); + } + return true; + }else { + return false; + } + } + + public void handleNewClient(EaglerSPClient client) { + synchronized(clients) { + clients.put(client.id, client); + send(new RelayPacket02NewClient(client.id)); + EaglerSPRelay.logger.debug("[{}][Relay -> Server] PKT 0x02: Notify server of the client, id: {}", serverAddress, client.id); + } + } + + public void handleClientDisconnect(EaglerSPClient client) { + synchronized(clients) { + clients.remove(client.id); + } + if(!client.serverNotifiedOfClose) { + send(new RelayPacketFEDisconnectClient(client.id, RelayPacketFEDisconnectClient.TYPE_UNKNOWN, "End of stream")); + client.serverNotifiedOfClose = true; + } + } + + public void handleClientICECandidate(EaglerSPClient client, RelayPacket03ICECandidate packet) { + send(new RelayPacket03ICECandidate(client.id, packet.candidate)); + } + + public void handleClientDescription(EaglerSPClient client, RelayPacket04Description packet) { + send(new RelayPacket04Description(client.id, packet.description)); + } + + public void handleClientSuccess(EaglerSPClient client, RelayPacket05ClientSuccess packet) { + send(new RelayPacket05ClientSuccess(client.id)); + } + + public void handleClientFailure(EaglerSPClient client, RelayPacket06ClientFailure packet) { + send(new RelayPacket06ClientFailure(client.id)); + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/LoginState.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/LoginState.java new file mode 100644 index 0000000..06f6366 --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/LoginState.java @@ -0,0 +1,37 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import net.lax1dude.eaglercraft.v1_8.sp.relay.pkt.RelayPacketFEDisconnectClient; + +/** + * Copyright (c) 2022 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *

+ * SENT = Client has sent something to the server
+ * RECIEVED = Server has sent something to the client + */ +public enum LoginState { + + INIT, SENT_ICE_CANDIDATE, RECIEVED_ICE_CANIDATE, SENT_DESCRIPTION, RECIEVED_DESCRIPTION, FINISHED; + + public static boolean assertEquals(EaglerSPClient client, LoginState state) { + if(client.state != state) { + String msg = "client is in state " + client.state.name() + " when it was supposed to be " + state.name(); + client.disconnect(RelayPacketFEDisconnectClient.TYPE_INVALID_OPERATION, msg); + EaglerSPRelay.logger.debug("[{}][Relay -> Client] PKT 0xFE: TYPE_INVALID_OPERATION: {}", (String) client.socket.getAttachment(), msg); + return false; + }else { + return true; + } + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/RateLimiter.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/RateLimiter.java new file mode 100644 index 0000000..56ea8ae --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/RateLimiter.java @@ -0,0 +1,128 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Copyright (c) 2022 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class RateLimiter { + + private final int period; + private final int limit; + private final int lockoutLimit; + private final int lockoutDuration; + + private class RateLimitEntry { + + protected long timer; + protected int count; + protected long lockedTimer; + protected boolean locked; + + protected RateLimitEntry() { + timer = System.currentTimeMillis(); + count = 0; + lockedTimer = 0l; + locked = false; + } + + protected void update() { + long millis = System.currentTimeMillis(); + if(locked) { + if(millis - lockedTimer > RateLimiter.this.lockoutDuration) { + timer = millis; + count = 0; + lockedTimer = 0l; + locked = false; + } + }else { + long p = RateLimiter.this.period / RateLimiter.this.limit; + int breaker = 0; + while(millis - timer > p) { + timer += p; + --count; + if(count < 0 || ++breaker > 100) { + timer = millis; + count = 0; + break; + } + } + } + } + + } + + public static enum RateLimit { + NONE, LIMIT, LIMIT_NOW_LOCKOUT, LOCKOUT; + } + + private final Map limiters = new HashMap(); + + public RateLimiter(int period, int limit, int lockoutLimit, int lockoutDuration) { + this.period = period; + this.limit = limit; + this.lockoutLimit = lockoutLimit; + this.lockoutDuration = lockoutDuration; + } + + public RateLimit limit(String addr) { + synchronized(this) { + RateLimitEntry etr = limiters.get(addr); + + if(etr == null) { + etr = new RateLimitEntry(); + limiters.put(addr, etr); + }else { + etr.update(); + } + + if(etr.locked) { + return RateLimit.LOCKOUT; + } + + ++etr.count; + if(etr.count >= lockoutLimit) { + etr.count = 0; + etr.locked = true; + etr.lockedTimer = System.currentTimeMillis(); + return RateLimit.LIMIT_NOW_LOCKOUT; + }else if(etr.count > limit) { + return RateLimit.LIMIT; + }else { + return RateLimit.NONE; + } + } + } + + public void update() { + synchronized(this) { + Iterator itr = limiters.values().iterator(); + while(itr.hasNext()) { + if(itr.next().count == 0) { + itr.remove(); + } + } + } + } + + public void reset() { + synchronized(this) { + limiters.clear(); + } + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/Util.java b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/Util.java new file mode 100644 index 0000000..7513712 --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/java/net/lax1dude/eaglercraft/v1_8/sp/relay/server/Util.java @@ -0,0 +1,33 @@ +package net.lax1dude.eaglercraft.v1_8.sp.relay.server; + +import java.net.InetSocketAddress; + +/** + * Copyright (c) 2022 lax1dude. All Rights Reserved. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ +public class Util { + + public static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + } + } + + public static String sock2String(InetSocketAddress sock) { + return sock.getAddress().getHostAddress() + ":" + sock.getPort(); + } + +} diff --git a/sp-relay/SharedWorldRelay/src/main/resources/relays.txt b/sp-relay/SharedWorldRelay/src/main/resources/relays.txt new file mode 100644 index 0000000..06308f4 --- /dev/null +++ b/sp-relay/SharedWorldRelay/src/main/resources/relays.txt @@ -0,0 +1,28 @@ + +[STUN] +url=stun:stun.l.google.com:19302 + +[STUN] +url=stun:stun1.l.google.com:19302 + +[STUN] +url=stun:stun2.l.google.com:19302 + +[STUN] +url=stun:stun3.l.google.com:19302 + +[STUN] +url=stun:stun4.l.google.com:19302 + +[STUN] +url=stun:openrelay.metered.ca:80 + +[TURN] +url=turn:openrelay.metered.ca:443 +username=openrelayproject +password=openrelayproject + +[TURN] +url=turn:openrelay.metered.ca:443?transport=tcp +username=openrelayproject +password=openrelayproject