{0.2a} Add shared world relay source (from 1.5.2)
This commit is contained in:
parent
20799a0856
commit
9433fd8251
11
sp-relay/SharedWorldRelay/.gitignore
vendored
Normal file
11
sp-relay/SharedWorldRelay/.gitignore
vendored
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
lib/*
|
||||||
|
.idea/*
|
||||||
|
*.iml
|
||||||
|
out/*
|
||||||
|
deps/BungeeCord.jar
|
||||||
|
/.gradle/
|
||||||
|
/.settings/
|
||||||
|
.classpath
|
||||||
|
.project
|
||||||
|
/build/
|
||||||
|
/bin/
|
46
sp-relay/SharedWorldRelay/build.gradle
Normal file
46
sp-relay/SharedWorldRelay/build.gradle
Normal file
|
@ -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'
|
||||||
|
}
|
BIN
sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
sp-relay/SharedWorldRelay/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -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
|
234
sp-relay/SharedWorldRelay/gradlew
vendored
Normal file
234
sp-relay/SharedWorldRelay/gradlew
vendored
Normal file
|
@ -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" "$@"
|
89
sp-relay/SharedWorldRelay/gradlew.bat
vendored
Normal file
89
sp-relay/SharedWorldRelay/gradlew.bat
vendored
Normal file
|
@ -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
|
2
sp-relay/SharedWorldRelay/settings.gradle
Normal file
2
sp-relay/SharedWorldRelay/settings.gradle
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
rootProject.name = 'SharedWorldRelay'
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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<String,DebugLogger> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<Entry<WebSocket,PendingConnection>> itr = pendingConnections.entrySet().iterator();
|
||||||
|
while(itr.hasNext()) {
|
||||||
|
Entry<WebSocket,PendingConnection> etr = itr.next();
|
||||||
|
if(millis - etr.getValue().openTime > 500l) {
|
||||||
|
etr.getKey().close();
|
||||||
|
itr.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized(clientConnections) {
|
||||||
|
Iterator<EaglerSPClient> 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<WebSocket,PendingConnection> pendingConnections = new HashMap();
|
||||||
|
private static final Map<String,EaglerSPClient> clientIds = new HashMap();
|
||||||
|
private static final Map<WebSocket,EaglerSPClient> clientConnections = new HashMap();
|
||||||
|
private static final Map<String,EaglerSPServer> serverCodes = new HashMap();
|
||||||
|
private static final Map<WebSocket,EaglerSPServer> serverConnections = new HashMap();
|
||||||
|
private static final Map<String,List<EaglerSPClient>> clientAddressSets = new HashMap();
|
||||||
|
private static final Map<String,List<EaglerSPServer>> 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<PendingConnection> pendingItr = pendingConnections.values().iterator();
|
||||||
|
while(pendingItr.hasNext()) {
|
||||||
|
if(pendingItr.next().address.equals(addr)) {
|
||||||
|
++totalCons;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized(clientAddressSets) {
|
||||||
|
List<EaglerSPClient> 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<EaglerSPServer> 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<EaglerSPServer> 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<EaglerSPClient> 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<EaglerSPServer> lst = serverAddressSets.get(srv.serverAddress);
|
||||||
|
if(lst != null) {
|
||||||
|
lst.remove(srv);
|
||||||
|
if(lst.size() == 0) {
|
||||||
|
serverAddressSets.remove(srv.serverAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ArrayList<EaglerSPClient> clientList;
|
||||||
|
synchronized(clientConnections) {
|
||||||
|
clientList = new ArrayList(clientConnections.values());
|
||||||
|
}
|
||||||
|
Iterator<EaglerSPClient> 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<EaglerSPClient> 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<RelayPacket07LocalWorlds.LocalWorld> getLocalWorlds(String addr) {
|
||||||
|
List<RelayPacket07LocalWorlds.LocalWorld> lst = new ArrayList();
|
||||||
|
synchronized(serverAddressSets) {
|
||||||
|
List<EaglerSPServer> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<RelayPacket01ICEServers.RelayServer> relayServers = new ArrayList();
|
||||||
|
|
||||||
|
public static void loadRelays(File list) throws IOException {
|
||||||
|
ArrayList<RelayPacket01ICEServers.RelayServer> 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<RelayPacket01ICEServers.RelayServer> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String,EaglerSPClient> 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <br><br>
|
||||||
|
* SENT = Client has sent something to the server<br>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String, RateLimitEntry> 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<RateLimitEntry> itr = limiters.values().iterator();
|
||||||
|
while(itr.hasNext()) {
|
||||||
|
if(itr.next().count == 0) {
|
||||||
|
itr.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
synchronized(this) {
|
||||||
|
limiters.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
28
sp-relay/SharedWorldRelay/src/main/resources/relays.txt
Normal file
28
sp-relay/SharedWorldRelay/src/main/resources/relays.txt
Normal file
|
@ -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
|
Loading…
Reference in New Issue
Block a user