mirror of
https://github.com/Eaglercraft-Archive/Eaglercraftx-1.8.8-src.git
synced 2025-06-27 18:38:14 -05:00
<1.0.0> Add experimental Velocity plugin files
Credit to ayunami2000 for making it work, not recommended for public servers yet. Download in "gateway/EaglercraftXVelocity/EaglerXVelocity-Latest.jar" Please check for updates regularly if you decide to use it.
This commit is contained in:
4
gateway/EaglercraftXVelocity/.gitignore
vendored
Normal file
4
gateway/EaglercraftXVelocity/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
lib/*
|
||||
.idea/*
|
||||
*.iml
|
||||
out/*
|
BIN
gateway/EaglercraftXVelocity/EaglerXVelocity-Latest.jar
Normal file
BIN
gateway/EaglercraftXVelocity/EaglerXVelocity-Latest.jar
Normal file
Binary file not shown.
11
gateway/EaglercraftXVelocity/readme.txt
Normal file
11
gateway/EaglercraftXVelocity/readme.txt
Normal file
@ -0,0 +1,11 @@
|
||||
Plugin for eaglercraft on velocity
|
||||
|
||||
Still in development, not recommended for public servers!
|
||||
|
||||
Not using gradle to give more direct access to velocity's internals, as gradle only provides a dummy jar containing the api.
|
||||
|
||||
EaglercraftXVelocity requires netty's websocket client/server, which is already in the production velocity jar so it's ideal to compile directly against the real jar
|
||||
|
||||
Simply link "src/main/java" and "src/main/resources" as source folders, and then add the latest version of velocity jar for minecraft 1.8 to the build path.
|
||||
|
||||
To build, export the source folders as a JAR and export the JAR to contain all the classes found in the JARs in "deps" within it, but not including the classes from the actual velocity jar
|
@ -0,0 +1,404 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
||||
import com.velocitypowered.proxy.network.TransportType;
|
||||
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFactory;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.ServerSocketChannel;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth.DefaultAuthSystem;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command.CommandConfirmCode;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command.CommandDomain;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command.CommandEaglerPurge;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command.CommandEaglerRegister;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command.CommandRatelimit;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command.EaglerCommand;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerAuthConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerVelocityConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.handlers.EaglerPacketEventListener;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web.HttpWebServer;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.shit.CompatWarning;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.BinaryHttpClient;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.CapeServiceOffline;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.ISkinService;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.SkinService;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.SkinServiceOffline;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.voice.VoiceService;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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.
|
||||
*
|
||||
*/
|
||||
@Plugin(
|
||||
id = EaglerXVelocityVersion.PLUGIN_ID,
|
||||
name = EaglerXVelocityVersion.NAME,
|
||||
description = EaglerXVelocityVersion.DESCRIPTION,
|
||||
version = EaglerXVelocityVersion.VERSION,
|
||||
authors = {
|
||||
"lax1dude",
|
||||
"ayunami2000"
|
||||
}
|
||||
)
|
||||
public class EaglerXVelocity {
|
||||
|
||||
static {
|
||||
CompatWarning.displayCompatWarning();
|
||||
}
|
||||
|
||||
private static EaglerXVelocity instance = null;
|
||||
private final VelocityServer proxy;
|
||||
private final Logger logger;
|
||||
private final Path dataDirAsPath;
|
||||
private final File dataDir;
|
||||
private ConnectionManager cm;
|
||||
private EaglerVelocityConfig conf = null;
|
||||
private EventLoopGroup bossGroup;
|
||||
private EventLoopGroup workerGroup;
|
||||
private TransportType transportType;
|
||||
private ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory;
|
||||
private ChannelFactory<? extends SocketChannel> socketChannelFactory;
|
||||
private ChannelFactory<? extends DatagramChannel> datagramChannelFactory;
|
||||
private Collection<Channel> openChannels;
|
||||
private Timer closeInactiveConnections;
|
||||
private Timer skinServiceTasks = null;
|
||||
private Timer authServiceTasks = null;
|
||||
private final ChannelFutureListener newChannelListener;
|
||||
private ISkinService skinService;
|
||||
private CapeServiceOffline capeService;
|
||||
private VoiceService voiceService;
|
||||
private DefaultAuthSystem defaultAuthSystem;
|
||||
|
||||
@Inject
|
||||
public EaglerXVelocity(ProxyServer proxyIn, Logger loggerIn, @DataDirectory Path dataDirIn) {
|
||||
instance = this;
|
||||
proxy = (VelocityServer)proxyIn;
|
||||
logger = loggerIn;
|
||||
dataDirAsPath = dataDirIn;
|
||||
dataDir = dataDirIn.toFile();
|
||||
|
||||
openChannels = new LinkedList();
|
||||
newChannelListener = new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture ch) throws Exception {
|
||||
synchronized(openChannels) { // synchronize whole block to preserve logging order
|
||||
if(ch.isSuccess()) {
|
||||
EaglerXVelocity.logger().info("Eaglercraft is listening on: {}", ch.channel().attr(EaglerPipeline.LOCAL_ADDRESS).get().toString());
|
||||
openChannels.add(ch.channel());
|
||||
}else {
|
||||
EaglerXVelocity.logger().error("Eaglercraft could not bind port: {}", ch.channel().attr(EaglerPipeline.LOCAL_ADDRESS).get().toString());
|
||||
EaglerXVelocity.logger().error("Reason: {}", ch.cause().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onProxyInit(ProxyInitializeEvent e) {
|
||||
logger.warn("This plugin is still in development, certain features may not be functional!");
|
||||
logger.warn("Not recommended for public servers yet, please check for updates");
|
||||
try {
|
||||
Field f = VelocityServer.class.getDeclaredField("cm");
|
||||
f.setAccessible(true);
|
||||
cm = (ConnectionManager)f.get(proxy);
|
||||
f = ConnectionManager.class.getDeclaredField("bossGroup");
|
||||
f.setAccessible(true);
|
||||
bossGroup = (EventLoopGroup)f.get(cm);
|
||||
f = ConnectionManager.class.getDeclaredField("workerGroup");
|
||||
f.setAccessible(true);
|
||||
workerGroup = (EventLoopGroup)f.get(cm);
|
||||
f = ConnectionManager.class.getDeclaredField("transportType");
|
||||
f.setAccessible(true);
|
||||
transportType = (TransportType)f.get(cm);
|
||||
f = TransportType.class.getDeclaredField("serverSocketChannelFactory");
|
||||
f.setAccessible(true);
|
||||
serverSocketChannelFactory = (ChannelFactory<? extends ServerSocketChannel>)f.get(transportType);
|
||||
f = TransportType.class.getDeclaredField("socketChannelFactory");
|
||||
f.setAccessible(true);
|
||||
socketChannelFactory = (ChannelFactory<? extends SocketChannel>)f.get(transportType);
|
||||
f = TransportType.class.getDeclaredField("datagramChannelFactory");
|
||||
f.setAccessible(true);
|
||||
datagramChannelFactory = (ChannelFactory<? extends DatagramChannel>)f.get(transportType);
|
||||
} catch(Throwable t) {
|
||||
throw new RuntimeException("Accessing private fields failed!", t);
|
||||
}
|
||||
reloadConfig();
|
||||
proxy.getEventManager().register(this, new EaglerPacketEventListener(this));
|
||||
EaglerCommand.register(this, new CommandRatelimit());
|
||||
EaglerCommand.register(this, new CommandConfirmCode());
|
||||
EaglerCommand.register(this, new CommandDomain());
|
||||
EaglerAuthConfig authConf = conf.getAuthConfig();
|
||||
if(authConf.isEnableAuthentication() && authConf.isUseBuiltInAuthentication()) {
|
||||
if(!proxy.getConfiguration().isOnlineMode()) {
|
||||
logger.error("Online mode is set to false! Authentication system has been disabled");
|
||||
authConf.triggerOnlineModeDisabled();
|
||||
}else {
|
||||
EaglerCommand.register(this, new CommandEaglerRegister(authConf.getEaglerCommandName()));
|
||||
EaglerCommand.register(this, new CommandEaglerPurge(authConf.getEaglerCommandName()));
|
||||
}
|
||||
}
|
||||
proxy.getChannelRegistrar().register(SkinService.CHANNEL, CapeServiceOffline.CHANNEL,
|
||||
EaglerPipeline.UPDATE_CERT_CHANNEL, VoiceService.CHANNEL,
|
||||
EaglerPacketEventListener.FNAW_SKIN_ENABLE_CHANNEL, EaglerPacketEventListener.GET_DOMAIN_CHANNEL);
|
||||
|
||||
if(closeInactiveConnections != null) {
|
||||
closeInactiveConnections.cancel();
|
||||
closeInactiveConnections = null;
|
||||
}
|
||||
closeInactiveConnections = new Timer(EaglerXVelocityVersion.ID + ": Network Tick Tasks");
|
||||
closeInactiveConnections.scheduleAtFixedRate(EaglerPipeline.closeInactive, 0l, 250l);
|
||||
|
||||
startListeners();
|
||||
|
||||
if(skinServiceTasks != null) {
|
||||
skinServiceTasks.cancel();
|
||||
skinServiceTasks = null;
|
||||
}
|
||||
boolean downloadSkins = conf.getDownloadVanillaSkins();
|
||||
if(downloadSkins) {
|
||||
if(skinService == null) {
|
||||
skinService = new SkinService();
|
||||
}else if(skinService instanceof SkinServiceOffline) {
|
||||
skinService.shutdown();
|
||||
skinService = new SkinService();
|
||||
}
|
||||
} else {
|
||||
if(skinService == null) {
|
||||
skinService = new SkinServiceOffline();
|
||||
}else if(skinService instanceof SkinService) {
|
||||
skinService.shutdown();
|
||||
skinService = new SkinServiceOffline();
|
||||
}
|
||||
}
|
||||
skinService.init(conf.getSkinCacheURI(), conf.getSQLiteDriverClass(), conf.getSQLiteDriverPath(),
|
||||
conf.getKeepObjectsDays(), conf.getKeepProfilesDays(), conf.getMaxObjects(), conf.getMaxProfiles());
|
||||
if(skinService instanceof SkinService) {
|
||||
skinServiceTasks = new Timer(EaglerXVelocityVersion.ID + ": Skin Service Tasks");
|
||||
skinServiceTasks.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
skinService.flush();
|
||||
}catch(Throwable t) {
|
||||
logger.error("Error flushing skin cache!", t);
|
||||
}
|
||||
}
|
||||
}, 1000l, 1000l);
|
||||
}
|
||||
capeService = new CapeServiceOffline();
|
||||
if(authConf.isEnableAuthentication() && authConf.isUseBuiltInAuthentication()) {
|
||||
try {
|
||||
defaultAuthSystem = DefaultAuthSystem.initializeAuthSystem(authConf);
|
||||
}catch(DefaultAuthSystem.AuthSystemException ex) {
|
||||
logger.error("Could not load authentication system!", ex);
|
||||
}
|
||||
if(defaultAuthSystem != null) {
|
||||
authServiceTasks = new Timer(EaglerXVelocityVersion.ID + ": Auth Service Tasks");
|
||||
authServiceTasks.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
defaultAuthSystem.flush();
|
||||
}catch(Throwable t) {
|
||||
logger.error("Error flushing auth cache!", t);
|
||||
}
|
||||
}
|
||||
}, 60000l, 60000l);
|
||||
}
|
||||
}
|
||||
if(conf.getEnableVoiceChat()) {
|
||||
voiceService = new VoiceService(conf);
|
||||
logger.warn("Voice chat enabled, not recommended for public servers!");
|
||||
}else {
|
||||
logger.info("Voice chat disabled, add \"allow_voice: true\" to your listeners to enable");
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onProxyShutdown(ProxyShutdownEvent e) {
|
||||
stopListeners();
|
||||
if(closeInactiveConnections != null) {
|
||||
closeInactiveConnections.cancel();
|
||||
closeInactiveConnections = null;
|
||||
}
|
||||
if(skinServiceTasks != null) {
|
||||
skinServiceTasks.cancel();
|
||||
skinServiceTasks = null;
|
||||
}
|
||||
skinService.shutdown();
|
||||
skinService = null;
|
||||
capeService.shutdown();
|
||||
capeService = null;
|
||||
if(defaultAuthSystem != null) {
|
||||
defaultAuthSystem.destroy();
|
||||
defaultAuthSystem = null;
|
||||
if(authServiceTasks != null) {
|
||||
authServiceTasks.cancel();
|
||||
authServiceTasks = null;
|
||||
}
|
||||
}
|
||||
voiceService = null;
|
||||
BinaryHttpClient.killEventLoop();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
stopListeners();
|
||||
reloadConfig();
|
||||
startListeners();
|
||||
}
|
||||
|
||||
private void reloadConfig() {
|
||||
try {
|
||||
conf = EaglerVelocityConfig.loadConfig(dataDir);
|
||||
if(conf == null) {
|
||||
throw new IOException("Config failed to parse!");
|
||||
}
|
||||
HttpWebServer.regenerate404Pages();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void startListeners() {
|
||||
for(EaglerListenerConfig conf : conf.getServerListeners()) {
|
||||
if(conf.getAddress() != null) {
|
||||
makeListener(conf, conf.getAddress());
|
||||
}
|
||||
if(conf.getAddressV6() != null) {
|
||||
makeListener(conf, conf.getAddressV6());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void makeListener(EaglerListenerConfig confData, InetSocketAddress addr) {
|
||||
ServerBootstrap bootstrap = new ServerBootstrap();
|
||||
bootstrap.option(ChannelOption.SO_REUSEADDR, true)
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.channelFactory(serverSocketChannelFactory)
|
||||
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, EaglerPipeline.SERVER_WRITE_MARK)
|
||||
.group(bossGroup, workerGroup)
|
||||
.childAttr(EaglerPipeline.LISTENER, confData)
|
||||
.attr(EaglerPipeline.LOCAL_ADDRESS, addr)
|
||||
.childOption(ChannelOption.IP_TOS, 24)
|
||||
.localAddress(addr)
|
||||
.childHandler(EaglerPipeline.SERVER_CHILD);
|
||||
if (this.proxy.getConfiguration().useTcpFastOpen()) {
|
||||
bootstrap.option(ChannelOption.TCP_FASTOPEN, 3);
|
||||
}
|
||||
bootstrap.bind().addListener(newChannelListener);
|
||||
}
|
||||
|
||||
public void stopListeners() {
|
||||
synchronized(openChannels) {
|
||||
for(Channel c : openChannels) {
|
||||
c.close().syncUninterruptibly();
|
||||
EaglerXVelocity.logger().info("Eaglercraft listener closed: " + c.attr(EaglerPipeline.LOCAL_ADDRESS).get().toString());
|
||||
}
|
||||
openChannels.clear();
|
||||
}
|
||||
synchronized(EaglerPipeline.openChannels) {
|
||||
EaglerPipeline.openChannels.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public EaglerVelocityConfig getConfig() {
|
||||
return conf;
|
||||
}
|
||||
|
||||
public EventLoopGroup getBossEventLoopGroup() {
|
||||
return bossGroup;
|
||||
}
|
||||
|
||||
public EventLoopGroup getWorkerEventLoopGroup() {
|
||||
return workerGroup;
|
||||
}
|
||||
|
||||
public TransportType getTransportType() {
|
||||
return transportType;
|
||||
}
|
||||
|
||||
public ChannelFactory<? extends SocketChannel> getChannelFactory() {
|
||||
return socketChannelFactory;
|
||||
}
|
||||
|
||||
public ISkinService getSkinService() {
|
||||
return skinService;
|
||||
}
|
||||
|
||||
public CapeServiceOffline getCapeService() {
|
||||
return capeService;
|
||||
}
|
||||
|
||||
public DefaultAuthSystem getAuthService() {
|
||||
return defaultAuthSystem;
|
||||
}
|
||||
|
||||
public VoiceService getVoiceService() {
|
||||
return voiceService;
|
||||
}
|
||||
|
||||
public static EaglerXVelocity getEagler() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public VelocityServer getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
public Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
public File getDataFolder() {
|
||||
return dataDir;
|
||||
}
|
||||
|
||||
public static VelocityServer proxy() {
|
||||
return instance.getProxy();
|
||||
}
|
||||
|
||||
public static Logger logger() {
|
||||
return instance.getLogger();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 EaglerXVelocityVersion {
|
||||
|
||||
public static final String NATIVE_VELOCITY_BUILD = "3.3.0-SNAPSHOT:afd8b55:b390";
|
||||
|
||||
public static final String ID = "EaglerXVelocity";
|
||||
public static final String PLUGIN_ID = "eaglerxvelocity";
|
||||
public static final String NAME = "EaglercraftXVelocity";
|
||||
public static final String DESCRIPTION = "Plugin to allow EaglercraftX 1.8 players to join your network, or allow EaglercraftX 1.8 players to use your network as a proxy to join other networks";
|
||||
public static final String VERSION = "1.0.0";
|
||||
public static final String[] AUTHORS = new String[] { "lax1dude", "ayunami2000" };
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPlayerData;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 EaglerXVelocityAPIHelper {
|
||||
|
||||
public static EaglerPlayerData getEaglerHandle(Player player) {
|
||||
return EaglerPipeline.getEaglerHandle(player);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 EaglercraftHandleAuthPasswordEvent {
|
||||
|
||||
public static enum AuthResponse {
|
||||
ALLOW, DENY
|
||||
}
|
||||
|
||||
private final EaglerListenerConfig listener;
|
||||
private final InetAddress authRemoteAddress;
|
||||
private final String authOrigin;
|
||||
private final byte[] authUsername;
|
||||
private final byte[] authSaltingData;
|
||||
private final byte[] authPasswordData;
|
||||
private final EaglercraftIsAuthRequiredEvent.AuthMethod eventAuthMethod;
|
||||
private final String eventAuthMessage;
|
||||
private final Object authAttachment;
|
||||
|
||||
private AuthResponse eventResponse;
|
||||
private CharSequence authProfileUsername;
|
||||
private UUID authProfileUUID;
|
||||
private String authRequestedServerRespose;
|
||||
private String authDeniedMessage = "Password Incorrect!";
|
||||
private String applyTexturesPropValue;
|
||||
private String applyTexturesPropSignature;
|
||||
private boolean overrideEaglerToVanillaSkins;
|
||||
private Consumer<EaglercraftHandleAuthPasswordEvent> continueThread;
|
||||
private Runnable continueRunnable;
|
||||
private volatile boolean hasContinue = false;
|
||||
|
||||
public EaglercraftHandleAuthPasswordEvent(EaglerListenerConfig listener, InetAddress authRemoteAddress,
|
||||
String authOrigin, byte[] authUsername, byte[] authSaltingData, CharSequence authProfileUsername,
|
||||
UUID authProfileUUID, byte[] authPasswordData, EaglercraftIsAuthRequiredEvent.AuthMethod eventAuthMethod,
|
||||
String eventAuthMessage, Object authAttachment, String authRequestedServerRespose,
|
||||
Consumer<EaglercraftHandleAuthPasswordEvent> continueThread) {
|
||||
this.listener = listener;
|
||||
this.authRemoteAddress = authRemoteAddress;
|
||||
this.authOrigin = authOrigin;
|
||||
this.authUsername = authUsername;
|
||||
this.authSaltingData = authSaltingData;
|
||||
this.authProfileUsername = authProfileUsername;
|
||||
this.authProfileUUID = authProfileUUID;
|
||||
this.authPasswordData = authPasswordData;
|
||||
this.eventAuthMethod = eventAuthMethod;
|
||||
this.eventAuthMessage = eventAuthMessage;
|
||||
this.authAttachment = authAttachment;
|
||||
this.authRequestedServerRespose = authRequestedServerRespose;
|
||||
this.continueThread = continueThread;
|
||||
}
|
||||
|
||||
public EaglerListenerConfig getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public InetAddress getRemoteAddress() {
|
||||
return authRemoteAddress;
|
||||
}
|
||||
|
||||
public String getOriginHeader() {
|
||||
return authOrigin;
|
||||
}
|
||||
|
||||
public byte[] getAuthUsername() {
|
||||
return authUsername;
|
||||
}
|
||||
|
||||
public byte[] getAuthSaltingData() {
|
||||
return authSaltingData;
|
||||
}
|
||||
|
||||
public CharSequence getProfileUsername() {
|
||||
return authProfileUsername;
|
||||
}
|
||||
|
||||
public void setProfileUsername(CharSequence username) {
|
||||
this.authProfileUsername = username;
|
||||
}
|
||||
|
||||
public UUID getProfileUUID() {
|
||||
return authProfileUUID;
|
||||
}
|
||||
|
||||
public void setProfileUUID(UUID uuid) {
|
||||
this.authProfileUUID = uuid;
|
||||
}
|
||||
|
||||
public byte[] getAuthPasswordDataResponse() {
|
||||
return authPasswordData;
|
||||
}
|
||||
|
||||
public EaglercraftIsAuthRequiredEvent.AuthMethod getAuthType() {
|
||||
return eventAuthMethod;
|
||||
}
|
||||
|
||||
public String getAuthMessage() {
|
||||
return eventAuthMessage;
|
||||
}
|
||||
|
||||
public <T> T getAuthAttachment() {
|
||||
return (T)authAttachment;
|
||||
}
|
||||
|
||||
public String getAuthRequestedServer() {
|
||||
return authRequestedServerRespose;
|
||||
}
|
||||
|
||||
public void setAuthRequestedServer(String server) {
|
||||
this.authRequestedServerRespose = server;
|
||||
}
|
||||
|
||||
public void setLoginAllowed() {
|
||||
this.eventResponse = AuthResponse.ALLOW;
|
||||
this.authDeniedMessage = null;
|
||||
}
|
||||
|
||||
public void setLoginDenied(String message) {
|
||||
this.eventResponse = AuthResponse.DENY;
|
||||
this.authDeniedMessage = message;
|
||||
}
|
||||
|
||||
public AuthResponse getLoginAllowed() {
|
||||
return eventResponse;
|
||||
}
|
||||
|
||||
public String getLoginDeniedMessage() {
|
||||
return authDeniedMessage;
|
||||
}
|
||||
|
||||
public Runnable makeAsyncContinue() {
|
||||
if(continueRunnable == null) {
|
||||
continueRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if(!hasContinue) {
|
||||
hasContinue = true;
|
||||
continueThread.accept(EaglercraftHandleAuthPasswordEvent.this);
|
||||
}else {
|
||||
throw new IllegalStateException("Thread was already continued from a different function! Auth plugin conflict?");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return continueRunnable;
|
||||
}
|
||||
|
||||
public boolean isAsyncContinue() {
|
||||
return continueRunnable != null;
|
||||
}
|
||||
|
||||
public void doDirectContinue() {
|
||||
continueThread.accept(this);
|
||||
}
|
||||
|
||||
public void applyTexturesProperty(String value, String signature) {
|
||||
applyTexturesPropValue = value;
|
||||
applyTexturesPropSignature = signature;
|
||||
}
|
||||
|
||||
public String getApplyTexturesPropertyValue() {
|
||||
return applyTexturesPropValue;
|
||||
}
|
||||
|
||||
public String getApplyTexturesPropertySignature() {
|
||||
return applyTexturesPropSignature;
|
||||
}
|
||||
|
||||
public void setOverrideEaglerToVanillaSkins(boolean overrideEaglerToVanillaSkins) {
|
||||
this.overrideEaglerToVanillaSkins = overrideEaglerToVanillaSkins;
|
||||
}
|
||||
|
||||
public boolean isOverrideEaglerToVanillaSkins() {
|
||||
return overrideEaglerToVanillaSkins;
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 EaglercraftIsAuthRequiredEvent {
|
||||
|
||||
public static enum AuthResponse {
|
||||
SKIP, REQUIRE, DENY
|
||||
}
|
||||
|
||||
public static enum AuthMethod {
|
||||
PLAINTEXT, EAGLER_SHA256, AUTHME_SHA256
|
||||
}
|
||||
|
||||
public EaglercraftIsAuthRequiredEvent(EaglerListenerConfig listener, InetAddress authRemoteAddress,
|
||||
String authOrigin, boolean wantsAuth, byte[] authUsername,
|
||||
Consumer<EaglercraftIsAuthRequiredEvent> continueThread) {
|
||||
this.listener = listener;
|
||||
this.authRemoteAddress = authRemoteAddress;
|
||||
this.authOrigin = authOrigin;
|
||||
this.wantsAuth = wantsAuth;
|
||||
this.authUsername = authUsername;
|
||||
this.continueThread = continueThread;
|
||||
}
|
||||
|
||||
private final EaglerListenerConfig listener;
|
||||
private AuthResponse authResponse;
|
||||
private final InetAddress authRemoteAddress;
|
||||
private final String authOrigin;
|
||||
private final boolean wantsAuth;
|
||||
private final byte[] authUsername;
|
||||
private byte[] authSaltingData;
|
||||
private AuthMethod eventAuthMethod = null;
|
||||
private String eventAuthMessage = "enter the code:";
|
||||
private String kickUserMessage = "Login Denied";
|
||||
private Object authAttachment;
|
||||
private Consumer<EaglercraftIsAuthRequiredEvent> continueThread;
|
||||
private Runnable continueRunnable;
|
||||
private volatile boolean hasContinue = false;
|
||||
|
||||
public EaglerListenerConfig getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public InetAddress getRemoteAddress() {
|
||||
return authRemoteAddress;
|
||||
}
|
||||
|
||||
public String getOriginHeader() {
|
||||
return authOrigin;
|
||||
}
|
||||
|
||||
public boolean isClientSolicitingPasscode() {
|
||||
return wantsAuth;
|
||||
}
|
||||
|
||||
public byte[] getAuthUsername() {
|
||||
return authUsername;
|
||||
}
|
||||
|
||||
public byte[] getSaltingData() {
|
||||
return authSaltingData;
|
||||
}
|
||||
|
||||
public void setSaltingData(byte[] saltingData) {
|
||||
authSaltingData = saltingData;
|
||||
}
|
||||
|
||||
public AuthMethod getUseAuthType() {
|
||||
return eventAuthMethod;
|
||||
}
|
||||
|
||||
public void setUseAuthMethod(AuthMethod authMethod) {
|
||||
this.eventAuthMethod = authMethod;
|
||||
}
|
||||
|
||||
public AuthResponse getAuthRequired() {
|
||||
return authResponse;
|
||||
}
|
||||
|
||||
public void setAuthRequired(AuthResponse required) {
|
||||
this.authResponse = required;
|
||||
}
|
||||
|
||||
public String getAuthMessage() {
|
||||
return eventAuthMessage;
|
||||
}
|
||||
|
||||
public void setAuthMessage(String eventAuthMessage) {
|
||||
this.eventAuthMessage = eventAuthMessage;
|
||||
}
|
||||
|
||||
public <T> T getAuthAttachment() {
|
||||
return (T)authAttachment;
|
||||
}
|
||||
|
||||
public void setAuthAttachment(Object authAttachment) {
|
||||
this.authAttachment = authAttachment;
|
||||
}
|
||||
|
||||
public boolean shouldKickUser() {
|
||||
return authResponse == null || authResponse == AuthResponse.DENY;
|
||||
}
|
||||
|
||||
public String getKickMessage() {
|
||||
return kickUserMessage;
|
||||
}
|
||||
|
||||
public void kickUser(String message) {
|
||||
authResponse = AuthResponse.DENY;
|
||||
kickUserMessage = message;
|
||||
}
|
||||
|
||||
public Runnable makeAsyncContinue() {
|
||||
if(continueRunnable == null) {
|
||||
continueRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if(!hasContinue) {
|
||||
hasContinue = true;
|
||||
continueThread.accept(EaglercraftIsAuthRequiredEvent.this);
|
||||
}else {
|
||||
throw new IllegalStateException("Thread was already continued from a different function! Auth plugin conflict?");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return continueRunnable;
|
||||
}
|
||||
|
||||
public boolean isAsyncContinue() {
|
||||
return continueRunnable != null;
|
||||
}
|
||||
|
||||
public void doDirectContinue() {
|
||||
continueThread.accept(this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.query.MOTDConnection;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 EaglercraftMOTDEvent {
|
||||
|
||||
protected final MOTDConnection connection;
|
||||
|
||||
public EaglercraftMOTDEvent(MOTDConnection connection) {
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
public InetAddress getRemoteAddress() {
|
||||
return connection.getAddress();
|
||||
}
|
||||
|
||||
public EaglerListenerConfig getListener() {
|
||||
return connection.getListener();
|
||||
}
|
||||
|
||||
public String getAccept() {
|
||||
return connection.getAccept();
|
||||
}
|
||||
|
||||
public MOTDConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 EaglercraftRegisterCapeEvent {
|
||||
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
private byte[] customTex = null;
|
||||
|
||||
public EaglercraftRegisterCapeEvent(String username, UUID uuid) {
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public void setForceUsePreset(int p) {
|
||||
customTex = new byte[5];
|
||||
customTex[0] = (byte)1;
|
||||
customTex[1] = (byte)(p >> 24);
|
||||
customTex[2] = (byte)(p >> 16);
|
||||
customTex[3] = (byte)(p >> 8);
|
||||
customTex[4] = (byte)(p & 0xFF);
|
||||
}
|
||||
|
||||
public void setForceUseCustom(byte[] tex) {
|
||||
customTex = new byte[1 + tex.length];
|
||||
customTex[0] = (byte)2;
|
||||
System.arraycopy(tex, 0, customTex, 1, tex.length);
|
||||
}
|
||||
|
||||
public void setForceUseCustomByPacket(byte[] packet) {
|
||||
customTex = packet;
|
||||
}
|
||||
|
||||
public byte[] getForceSetUseCustomPacket() {
|
||||
return customTex;
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.velocitypowered.api.util.GameProfile.Property;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 EaglercraftRegisterSkinEvent {
|
||||
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
private Property useMojangProfileProperty = null;
|
||||
private boolean useLoginResultTextures = false;
|
||||
private byte[] customTex = null;
|
||||
private String customURL = null;
|
||||
|
||||
public EaglercraftRegisterSkinEvent(String username, UUID uuid) {
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public void setForceUseMojangProfileProperty(Property prop) {
|
||||
useMojangProfileProperty = prop;
|
||||
useLoginResultTextures = false;
|
||||
customTex = null;
|
||||
customURL = null;
|
||||
}
|
||||
|
||||
public void setForceUseLoginResultObjectTextures(boolean b) {
|
||||
useMojangProfileProperty = null;
|
||||
useLoginResultTextures = b;
|
||||
customTex = null;
|
||||
customURL = null;
|
||||
}
|
||||
|
||||
public void setForceUsePreset(int p) {
|
||||
useMojangProfileProperty = null;
|
||||
useLoginResultTextures = false;
|
||||
customTex = new byte[5];
|
||||
customTex[0] = (byte)1;
|
||||
customTex[1] = (byte)(p >> 24);
|
||||
customTex[2] = (byte)(p >> 16);
|
||||
customTex[3] = (byte)(p >> 8);
|
||||
customTex[4] = (byte)(p & 0xFF);
|
||||
customURL = null;
|
||||
}
|
||||
|
||||
public void setForceUseCustom(int model, byte[] tex) {
|
||||
useMojangProfileProperty = null;
|
||||
useLoginResultTextures = false;
|
||||
customTex = new byte[2 + tex.length];
|
||||
customTex[0] = (byte)2;
|
||||
customTex[1] = (byte)model;
|
||||
System.arraycopy(tex, 0, customTex, 2, tex.length);
|
||||
customURL = null;
|
||||
}
|
||||
|
||||
public void setForceUseCustomByPacket(byte[] packet) {
|
||||
useMojangProfileProperty = null;
|
||||
useLoginResultTextures = false;
|
||||
customTex = packet;
|
||||
customURL = null;
|
||||
}
|
||||
|
||||
public void setForceUseURL(String url) {
|
||||
useMojangProfileProperty = null;
|
||||
useLoginResultTextures = false;
|
||||
customTex = null;
|
||||
customURL = url;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public Property getForceUseMojangProfileProperty() {
|
||||
return useMojangProfileProperty;
|
||||
}
|
||||
|
||||
public boolean getForceUseLoginResultObjectTextures() {
|
||||
return useLoginResultTextures;
|
||||
}
|
||||
|
||||
public byte[] getForceSetUseCustomPacket() {
|
||||
return customTex;
|
||||
}
|
||||
|
||||
public String getForceSetUseURL() {
|
||||
return customURL;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.query;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.HttpServerQueryHandler;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.query.QueryManager;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 abstract class EaglerQueryHandler extends HttpServerQueryHandler {
|
||||
|
||||
public static void registerQueryType(String name, Class<? extends EaglerQueryHandler> clazz) {
|
||||
QueryManager.registerQueryType(name, clazz);
|
||||
}
|
||||
|
||||
public static void unregisterQueryType(String name) {
|
||||
QueryManager.unregisterQueryType(name);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.query;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 abstract class EaglerQuerySimpleHandler extends EaglerQueryHandler {
|
||||
|
||||
@Override
|
||||
protected void processString(String str) {
|
||||
throw new UnexpectedDataException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processJson(JsonObject obj) {
|
||||
throw new UnexpectedDataException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processBytes(byte[] bytes) {
|
||||
throw new UnexpectedDataException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void acceptText() {
|
||||
throw new UnsupportedOperationException("EaglerQuerySimpleHandler does not support duplex");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void acceptText(boolean bool) {
|
||||
throw new UnsupportedOperationException("EaglerQuerySimpleHandler does not support duplex");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void acceptBinary() {
|
||||
throw new UnsupportedOperationException("EaglerQuerySimpleHandler does not support duplex");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void acceptBinary(boolean bool) {
|
||||
throw new UnsupportedOperationException("EaglerQuerySimpleHandler does not support duplex");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closed() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.query;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 interface MOTDConnection {
|
||||
|
||||
boolean isClosed();
|
||||
void close();
|
||||
|
||||
String getAccept();
|
||||
InetAddress getAddress();
|
||||
EaglerListenerConfig getListener();
|
||||
long getConnectionTimestamp();
|
||||
|
||||
public default long getConnectionAge() {
|
||||
return System.currentTimeMillis() - getConnectionTimestamp();
|
||||
}
|
||||
|
||||
void sendToUser();
|
||||
void setKeepAlive(boolean enable);
|
||||
|
||||
String getLine1();
|
||||
String getLine2();
|
||||
List<String> getPlayerList();
|
||||
int[] getBitmap();
|
||||
int getOnlinePlayers();
|
||||
int getMaxPlayers();
|
||||
String getSubType();
|
||||
|
||||
void setLine1(String p);
|
||||
void setLine2(String p);
|
||||
void setPlayerList(List<String> p);
|
||||
void setPlayerList(String... p);
|
||||
void setBitmap(int[] p);
|
||||
void setOnlinePlayers(int i);
|
||||
void setMaxPlayers(int i);
|
||||
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 AuthLoadingCache<K, V> {
|
||||
|
||||
private static class CacheEntry<V> {
|
||||
|
||||
private long lastHit;
|
||||
private V instance;
|
||||
|
||||
private CacheEntry(V instance) {
|
||||
this.lastHit = System.currentTimeMillis();
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static interface CacheLoader<K, V> {
|
||||
V load(K key);
|
||||
}
|
||||
|
||||
public static interface CacheVisitor<K, V> {
|
||||
boolean shouldEvict(K key, V value);
|
||||
}
|
||||
|
||||
private final Map<K, CacheEntry<V>> cacheMap;
|
||||
private final CacheLoader<K, V> provider;
|
||||
private final long cacheTTL;
|
||||
|
||||
private long cacheTimer;
|
||||
|
||||
public AuthLoadingCache(CacheLoader<K, V> provider, long cacheTTL) {
|
||||
this.cacheMap = new HashMap();
|
||||
this.provider = provider;
|
||||
this.cacheTTL = cacheTTL;
|
||||
}
|
||||
|
||||
public V get(K key) {
|
||||
CacheEntry<V> etr;
|
||||
synchronized(cacheMap) {
|
||||
etr = cacheMap.get(key);
|
||||
}
|
||||
if(etr == null) {
|
||||
V loaded = provider.load(key);
|
||||
synchronized(cacheMap) {
|
||||
cacheMap.put(key, new CacheEntry<>(loaded));
|
||||
}
|
||||
return loaded;
|
||||
}else {
|
||||
etr.lastHit = System.currentTimeMillis();
|
||||
return etr.instance;
|
||||
}
|
||||
}
|
||||
|
||||
public void evict(K key) {
|
||||
synchronized(cacheMap) {
|
||||
cacheMap.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void evictAll(CacheVisitor<K, V> visitor) {
|
||||
synchronized(cacheMap) {
|
||||
Iterator<Entry<K,CacheEntry<V>>> itr = cacheMap.entrySet().iterator();
|
||||
while(itr.hasNext()) {
|
||||
Entry<K,CacheEntry<V>> etr = itr.next();
|
||||
if(visitor.shouldEvict(etr.getKey(), etr.getValue().instance)) {
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
long millis = System.currentTimeMillis();
|
||||
if(millis - cacheTimer > (cacheTTL / 2L)) {
|
||||
cacheTimer = millis;
|
||||
synchronized(cacheMap) {
|
||||
Iterator<CacheEntry<V>> mapItr = cacheMap.values().iterator();
|
||||
while(mapItr.hasNext()) {
|
||||
CacheEntry<V> etr = mapItr.next();
|
||||
if(millis - etr.lastHit > cacheTTL) {
|
||||
mapItr.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
synchronized(cacheMap) {
|
||||
cacheMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,679 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Date;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.api.util.GameProfile.Property;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event.EaglercraftHandleAuthPasswordEvent;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event.EaglercraftIsAuthRequiredEvent;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event.EaglercraftIsAuthRequiredEvent.AuthMethod;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.event.EaglercraftIsAuthRequiredEvent.AuthResponse;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerAuthConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.Base64;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.sqlite.EaglerDrivers;
|
||||
|
||||
/**
|
||||
* 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 DefaultAuthSystem {
|
||||
|
||||
public static class AuthSystemException extends RuntimeException {
|
||||
|
||||
public AuthSystemException() {
|
||||
}
|
||||
|
||||
public AuthSystemException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AuthSystemException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthSystemException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected final String uri;
|
||||
protected final Connection databaseConnection;
|
||||
protected final String passwordPromptScreenText;
|
||||
protected final String wrongPasswordScreenText;
|
||||
protected final String notRegisteredScreenText;
|
||||
protected final String eaglerCommandName;
|
||||
protected final String useRegisterCommandText;
|
||||
protected final String useChangeCommandText;
|
||||
protected final String commandSuccessText;
|
||||
protected final String lastEaglerLoginMessage;
|
||||
protected final String tooManyRegistrationsMessage;
|
||||
protected final String needVanillaToRegisterMessage;
|
||||
protected final boolean overrideEaglerToVanillaSkins;
|
||||
protected final int maxRegistrationsPerIP;
|
||||
|
||||
protected final SecureRandom secureRandom;
|
||||
|
||||
public static DefaultAuthSystem initializeAuthSystem(EaglerAuthConfig config) throws AuthSystemException {
|
||||
String databaseURI = config.getDatabaseURI();
|
||||
Connection conn;
|
||||
try {
|
||||
conn = EaglerDrivers.connectToDatabase(databaseURI, config.getDriverClass(), config.getDriverPath(), new Properties());
|
||||
if(conn == null) {
|
||||
throw new IllegalStateException("Connection is null");
|
||||
}
|
||||
}catch(Throwable t) {
|
||||
throw new AuthSystemException("Could not initialize '" + databaseURI + "'!", t);
|
||||
}
|
||||
EaglerXVelocity.logger().info("Connected to database: " + databaseURI);
|
||||
try {
|
||||
try(Statement stmt = conn.createStatement()) {
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS \"eaglercraft_accounts\" ("
|
||||
+ "\"Version\" TINYINT NOT NULL,"
|
||||
+ "\"MojangUUID\" TEXT(32) NOT NULL,"
|
||||
+ "\"MojangUsername\" TEXT(16) NOT NULL,"
|
||||
+ "\"HashBase\" BLOB NOT NULL,"
|
||||
+ "\"HashSalt\" BLOB NOT NULL,"
|
||||
+ "\"MojangTextures\" BLOB,"
|
||||
+ "\"Registered\" DATETIME NOT NULL,"
|
||||
+ "\"RegisteredIP\" VARCHAR(42) NOT NULL,"
|
||||
+ "\"LastLogin\" DATETIME,"
|
||||
+ "\"LastLoginIP\" VARCHAR(42),"
|
||||
+ "PRIMARY KEY(\"MojangUUID\"))");
|
||||
stmt.execute("CREATE UNIQUE INDEX IF NOT EXISTS \"MojangUsername\" ON "
|
||||
+ "\"eaglercraft_accounts\" (\"MojangUsername\")");
|
||||
}
|
||||
return new DefaultAuthSystem(databaseURI, conn, config.getPasswordPromptScreenText(),
|
||||
config.getWrongPasswordScreenText(), config.getNotRegisteredScreenText(),
|
||||
config.getEaglerCommandName(), config.getUseRegisterCommandText(), config.getUseChangeCommandText(),
|
||||
config.getCommandSuccessText(), config.getLastEaglerLoginMessage(),
|
||||
config.getTooManyRegistrationsMessage(), config.getNeedVanillaToRegisterMessage(),
|
||||
config.getOverrideEaglerToVanillaSkins(), config.getMaxRegistrationsPerIP());
|
||||
}catch(AuthSystemException ex) {
|
||||
try {
|
||||
conn.close();
|
||||
}catch(SQLException exx) {
|
||||
}
|
||||
throw ex;
|
||||
}catch(Throwable t) {
|
||||
try {
|
||||
conn.close();
|
||||
}catch(SQLException exx) {
|
||||
}
|
||||
throw new AuthSystemException("Could not initialize '" + databaseURI + "'!", t);
|
||||
}
|
||||
}
|
||||
|
||||
protected final PreparedStatement registerUser;
|
||||
protected final PreparedStatement isRegisteredUser;
|
||||
protected final PreparedStatement pruneUsers;
|
||||
protected final PreparedStatement updatePassword;
|
||||
protected final PreparedStatement updateMojangUsername;
|
||||
protected final PreparedStatement getRegistrationsOnIP;
|
||||
protected final PreparedStatement checkRegistrationByUUID;
|
||||
protected final PreparedStatement checkRegistrationByName;
|
||||
protected final PreparedStatement setLastLogin;
|
||||
protected final PreparedStatement updateTextures;
|
||||
|
||||
protected class AccountLoader implements AuthLoadingCache.CacheLoader<String, CachedAccountInfo> {
|
||||
|
||||
@Override
|
||||
public CachedAccountInfo load(String key) {
|
||||
try {
|
||||
CachedAccountInfo cachedInfo = null;
|
||||
synchronized(checkRegistrationByName) {
|
||||
checkRegistrationByName.setString(1, key);
|
||||
try(ResultSet res = checkRegistrationByName.executeQuery()) {
|
||||
if (res.next()) {
|
||||
cachedInfo = new CachedAccountInfo(res.getInt(1), parseMojangUUID(res.getString(2)), key,
|
||||
res.getBytes(3), res.getBytes(4), res.getBytes(5), res.getDate(6), res.getString(7),
|
||||
res.getDate(8), res.getString(9));
|
||||
}
|
||||
}
|
||||
}
|
||||
return cachedInfo;
|
||||
}catch(SQLException ex) {
|
||||
throw new AuthException("Failed to query database!", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected class CachedAccountInfo {
|
||||
|
||||
protected int version;
|
||||
protected UUID mojangUUID;
|
||||
protected String mojangUsername;
|
||||
protected byte[] texturesProperty;
|
||||
protected byte[] hashBase;
|
||||
protected byte[] hashSalt;
|
||||
protected long registered;
|
||||
protected String registeredIP;
|
||||
protected long lastLogin;
|
||||
protected String lastLoginIP;
|
||||
|
||||
protected CachedAccountInfo(int version, UUID mojangUUID, String mojangUsername, byte[] texturesProperty,
|
||||
byte[] hashBase, byte[] hashSalt, Date registered, String registeredIP, Date lastLogin,
|
||||
String lastLoginIP) {
|
||||
this(version, mojangUUID, mojangUsername, texturesProperty, hashBase, hashSalt,
|
||||
registered == null ? 0l : registered.getTime(), registeredIP,
|
||||
lastLogin == null ? 0l : lastLogin.getTime(), lastLoginIP);
|
||||
}
|
||||
|
||||
protected CachedAccountInfo(int version, UUID mojangUUID, String mojangUsername, byte[] texturesProperty,
|
||||
byte[] hashBase, byte[] hashSalt, long registered, String registeredIP, long lastLogin,
|
||||
String lastLoginIP) {
|
||||
this.version = version;
|
||||
this.mojangUUID = mojangUUID;
|
||||
this.mojangUsername = mojangUsername;
|
||||
this.texturesProperty = texturesProperty;
|
||||
this.hashBase = hashBase;
|
||||
this.hashSalt = hashSalt;
|
||||
this.registered = registered;
|
||||
this.registeredIP = registeredIP;
|
||||
this.lastLogin = lastLogin;
|
||||
this.lastLoginIP = lastLoginIP;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected final AuthLoadingCache<String, CachedAccountInfo> authLoadingCache;
|
||||
|
||||
protected DefaultAuthSystem(String uri, Connection databaseConnection, String passwordPromptScreenText,
|
||||
String wrongPasswordScreenText, String notRegisteredScreenText, String eaglerCommandName,
|
||||
String useRegisterCommandText, String useChangeCommandText, String commandSuccessText,
|
||||
String lastEaglerLoginMessage, String tooManyRegistrationsMessage, String needVanillaToRegisterMessage,
|
||||
boolean overrideEaglerToVanillaSkins, int maxRegistrationsPerIP) throws SQLException {
|
||||
this.uri = uri;
|
||||
this.databaseConnection = databaseConnection;
|
||||
this.passwordPromptScreenText = passwordPromptScreenText;
|
||||
this.wrongPasswordScreenText = wrongPasswordScreenText;
|
||||
this.notRegisteredScreenText = notRegisteredScreenText;
|
||||
this.eaglerCommandName = eaglerCommandName;
|
||||
this.useRegisterCommandText = useRegisterCommandText;
|
||||
this.useChangeCommandText = useChangeCommandText;
|
||||
this.commandSuccessText = commandSuccessText;
|
||||
this.lastEaglerLoginMessage = lastEaglerLoginMessage;
|
||||
this.tooManyRegistrationsMessage = tooManyRegistrationsMessage;
|
||||
this.needVanillaToRegisterMessage = needVanillaToRegisterMessage;
|
||||
this.overrideEaglerToVanillaSkins = overrideEaglerToVanillaSkins;
|
||||
this.maxRegistrationsPerIP = maxRegistrationsPerIP;
|
||||
this.registerUser = databaseConnection.prepareStatement("INSERT INTO eaglercraft_accounts (Version, MojangUUID, MojangUsername, MojangTextures, HashBase, HashSalt, Registered, RegisteredIP) VALUES(?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
this.isRegisteredUser = databaseConnection.prepareStatement("SELECT COUNT(MojangUUID) AS total_accounts FROM eaglercraft_accounts WHERE MojangUUID = ?");
|
||||
this.pruneUsers = databaseConnection.prepareStatement("DELETE FROM eaglercraft_accounts WHERE LastLogin < ?");
|
||||
this.updatePassword = databaseConnection.prepareStatement("UPDATE eaglercraft_accounts SET HashBase = ?, HashSalt = ? WHERE MojangUUID = ?");
|
||||
this.updateMojangUsername = databaseConnection.prepareStatement("UPDATE eaglercraft_accounts SET MojangUsername = ? WHERE MojangUUID = ?");
|
||||
this.getRegistrationsOnIP = databaseConnection.prepareStatement("SELECT COUNT(MojangUUID) AS total_accounts FROM eaglercraft_accounts WHERE RegisteredIP = ?");
|
||||
this.checkRegistrationByUUID = databaseConnection.prepareStatement("SELECT Version, MojangUsername, LastLogin, LastLoginIP FROM eaglercraft_accounts WHERE MojangUUID = ?");
|
||||
this.checkRegistrationByName = databaseConnection.prepareStatement("SELECT Version, MojangUUID, MojangTextures, HashBase, HashSalt, Registered, RegisteredIP, LastLogin, LastLoginIP FROM eaglercraft_accounts WHERE MojangUsername = ?");
|
||||
this.setLastLogin = databaseConnection.prepareStatement("UPDATE eaglercraft_accounts SET LastLogin = ?, LastLoginIP = ? WHERE MojangUUID = ?");
|
||||
this.updateTextures = databaseConnection.prepareStatement("UPDATE eaglercraft_accounts SET MojangTextures = ? WHERE MojangUUID = ?");
|
||||
this.authLoadingCache = new AuthLoadingCache(new AccountLoader(), 120000l);
|
||||
this.secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
public void handleIsAuthRequiredEvent(EaglercraftIsAuthRequiredEvent event) {
|
||||
String username = new String(event.getAuthUsername(), StandardCharsets.US_ASCII);
|
||||
|
||||
String usrs = username.toString();
|
||||
if(!usrs.equals(usrs.replaceAll("[^A-Za-z0-9_]", "_").trim())) {
|
||||
event.kickUser("Invalid characters in username");
|
||||
return;
|
||||
}
|
||||
|
||||
if(username.length() < 3) {
|
||||
event.kickUser("Username must be at least 3 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
if(username.length() > 16) {
|
||||
event.kickUser("Username must be under 16 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
CachedAccountInfo info = authLoadingCache.get(username);
|
||||
if(info == null) {
|
||||
event.kickUser(notRegisteredScreenText);
|
||||
return;
|
||||
}
|
||||
|
||||
event.setAuthAttachment(info);
|
||||
event.setAuthRequired(AuthResponse.REQUIRE);
|
||||
event.setAuthMessage(passwordPromptScreenText);
|
||||
event.setUseAuthMethod(AuthMethod.EAGLER_SHA256);
|
||||
|
||||
byte[] randomBytes = new byte[32];
|
||||
Random rng;
|
||||
synchronized(secureRandom) {
|
||||
rng = new Random(secureRandom.nextLong());
|
||||
}
|
||||
|
||||
rng.nextBytes(randomBytes);
|
||||
|
||||
byte[] saltingData = new byte[64];
|
||||
System.arraycopy(info.hashSalt, 0, saltingData, 0, 32);
|
||||
System.arraycopy(randomBytes, 0, saltingData, 32, 32);
|
||||
|
||||
event.setSaltingData(saltingData);
|
||||
}
|
||||
|
||||
public void handleAuthPasswordEvent(EaglercraftHandleAuthPasswordEvent event) {
|
||||
CachedAccountInfo info = event.getAuthAttachment();
|
||||
|
||||
if(info == null) {
|
||||
event.setLoginDenied(notRegisteredScreenText);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
byte[] responseHash = event.getAuthPasswordDataResponse();
|
||||
|
||||
if(responseHash.length != 32) {
|
||||
event.setLoginDenied("Wrong number of bits in checksum!");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] saltingData = event.getAuthSaltingData();
|
||||
|
||||
SHA256Digest digest = new SHA256Digest();
|
||||
|
||||
digest.update(info.hashBase, 0, 32);
|
||||
digest.update(saltingData, 32, 32);
|
||||
digest.update(HashUtils.EAGLER_SHA256_SALT_BASE, 0, 32);
|
||||
|
||||
byte[] hashed = new byte[32];
|
||||
digest.doFinal(hashed, 0);
|
||||
|
||||
if(!Arrays.equals(hashed, responseHash)) {
|
||||
event.setLoginDenied(wrongPasswordScreenText);
|
||||
EaglerXVelocity.logger().warn("User \"{}\" entered the wrong password while logging in from: {}", info.mojangUsername, event.getRemoteAddress().getHostAddress());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
synchronized(setLastLogin) {
|
||||
setLastLogin.setDate(1, new Date(System.currentTimeMillis()));
|
||||
setLastLogin.setString(2, event.getRemoteAddress().getHostAddress());
|
||||
setLastLogin.setString(3, getMojangUUID(info.mojangUUID));
|
||||
if(setLastLogin.executeUpdate() == 0) {
|
||||
throw new SQLException("Query did not alter the database");
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
EaglerXVelocity.logger().error("Could not update last login for \"{}\"", info.mojangUUID.toString(), ex);
|
||||
}
|
||||
|
||||
event.setLoginAllowed();
|
||||
event.setProfileUsername(info.mojangUsername);
|
||||
event.setProfileUUID(info.mojangUUID);
|
||||
|
||||
byte[] texturesProp = info.texturesProperty;
|
||||
if(texturesProp != null) {
|
||||
try {
|
||||
DataInputStream dis = new DataInputStream(new ByteArrayInputStream(texturesProp));
|
||||
int valueLen = dis.readInt();
|
||||
int sigLen = dis.readInt();
|
||||
byte[] valueBytes = new byte[valueLen];
|
||||
dis.read(valueBytes);
|
||||
String valueB64 = Base64.encodeBase64String(valueBytes);
|
||||
String sigB64 = null;
|
||||
if(sigLen > 0) {
|
||||
valueBytes = new byte[sigLen];
|
||||
dis.read(valueBytes);
|
||||
sigB64 = Base64.encodeBase64String(valueBytes);
|
||||
}
|
||||
event.applyTexturesProperty(valueB64, sigB64);
|
||||
event.setOverrideEaglerToVanillaSkins(overrideEaglerToVanillaSkins);
|
||||
}catch(IOException ex) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void processSetPassword(ConnectedPlayer player, String password) throws TooManyRegisteredOnIPException, AuthException {
|
||||
if(EaglerPipeline.getEaglerHandle(player) != null) {
|
||||
throw new AuthException("Cannot register from an eaglercraft account!");
|
||||
}else if(!player.isOnlineMode()) {
|
||||
throw new AuthException("Cannot register without online mode enabled!");
|
||||
}else {
|
||||
try {
|
||||
String uuid = getMojangUUID(player.getUniqueId());
|
||||
synchronized(registerUser) {
|
||||
int cnt;
|
||||
synchronized(isRegisteredUser) {
|
||||
isRegisteredUser.setString(1, uuid);
|
||||
try(ResultSet set = isRegisteredUser.executeQuery()) {
|
||||
if(set.next()) {
|
||||
cnt = set.getInt(1);
|
||||
}else {
|
||||
throw new SQLException("Empty ResultSet recieved while checking if user exists");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SHA256Digest digest = new SHA256Digest();
|
||||
|
||||
int passLen = password.length();
|
||||
|
||||
digest.update((byte)((passLen >> 8) & 0xFF));
|
||||
digest.update((byte)(passLen & 0xFF));
|
||||
for(int i = 0; i < passLen; ++i) {
|
||||
char codePoint = password.charAt(i);
|
||||
digest.update((byte)((codePoint >> 8) & 0xFF));
|
||||
digest.update((byte)(codePoint & 0xFF));
|
||||
}
|
||||
|
||||
digest.update(HashUtils.EAGLER_SHA256_SALT_SAVE, 0, 32);
|
||||
|
||||
byte[] hashed = new byte[32];
|
||||
digest.doFinal(hashed, 0);
|
||||
|
||||
byte[] randomBytes = new byte[32];
|
||||
synchronized(secureRandom) {
|
||||
secureRandom.nextBytes(randomBytes);
|
||||
}
|
||||
|
||||
digest.reset();
|
||||
|
||||
digest.update(hashed, 0, 32);
|
||||
digest.update(randomBytes, 0, 32);
|
||||
digest.update(HashUtils.EAGLER_SHA256_SALT_BASE, 0, 32);
|
||||
|
||||
digest.doFinal(hashed, 0);
|
||||
|
||||
String username = player.getUsername();
|
||||
authLoadingCache.evict(username);
|
||||
|
||||
if(cnt > 0) {
|
||||
synchronized(updatePassword) {
|
||||
updatePassword.setBytes(1, hashed);
|
||||
updatePassword.setBytes(2, randomBytes);
|
||||
updatePassword.setString(3, uuid);
|
||||
if(updatePassword.executeUpdate() <= 0) {
|
||||
throw new AuthException("Update password query did not alter the database!");
|
||||
}
|
||||
}
|
||||
}else {
|
||||
String sockAddr = sockAddrToString(player.getRemoteAddress());
|
||||
if(maxRegistrationsPerIP > 0) {
|
||||
if(countUsersOnIP(sockAddr) >= maxRegistrationsPerIP) {
|
||||
throw new TooManyRegisteredOnIPException(sockAddr);
|
||||
}
|
||||
}
|
||||
Date nowDate = new Date(System.currentTimeMillis());
|
||||
registerUser.setInt(1, 1);
|
||||
registerUser.setString(2, uuid);
|
||||
registerUser.setString(3, username);
|
||||
GameProfile res = player.getGameProfile();
|
||||
if(res != null) {
|
||||
registerUser.setBytes(4, getTexturesProperty(res));
|
||||
}else {
|
||||
registerUser.setBytes(4, null);
|
||||
}
|
||||
registerUser.setBytes(5, hashed);
|
||||
registerUser.setBytes(6, randomBytes);
|
||||
registerUser.setDate(7, nowDate);
|
||||
registerUser.setString(8, sockAddr);
|
||||
if(registerUser.executeUpdate() <= 0) {
|
||||
throw new AuthException("Registration query did not alter the database!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
throw new AuthException("Failed to query database!", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] getTexturesProperty(GameProfile profile) {
|
||||
if(profile == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
List<Property> props = profile.getProperties();
|
||||
for(int i = 0, l = props.size(); i < l; ++i) {
|
||||
Property prop = props.get(i);
|
||||
if("textures".equals(prop.getName())) {
|
||||
byte[] texturesData = Base64.decodeBase64(prop.getValue());
|
||||
byte[] signatureData = prop.getSignature() == null ? new byte[0] : Base64.decodeBase64(prop.getSignature());
|
||||
ByteArrayOutputStream bao = new ByteArrayOutputStream();
|
||||
DataOutputStream dao = new DataOutputStream(bao);
|
||||
dao.writeInt(texturesData.length);
|
||||
dao.writeInt(signatureData.length);
|
||||
dao.write(texturesData);
|
||||
dao.write(signatureData);
|
||||
return bao.toByteArray();
|
||||
}
|
||||
}
|
||||
}catch(Throwable t) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int pruneUsers(long before) throws AuthException {
|
||||
try {
|
||||
authLoadingCache.flush();
|
||||
synchronized(pruneUsers) {
|
||||
pruneUsers.setDate(1, new Date(before));
|
||||
return pruneUsers.executeUpdate();
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
throw new AuthException("Failed to query database!", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public int countUsersOnIP(String addr) throws AuthException {
|
||||
synchronized(getRegistrationsOnIP) {
|
||||
try {
|
||||
getRegistrationsOnIP.setString(1, addr);
|
||||
try(ResultSet set = getRegistrationsOnIP.executeQuery()) {
|
||||
if(set.next()) {
|
||||
return set.getInt(1);
|
||||
}else {
|
||||
throw new SQLException("Empty ResultSet recieved while counting accounts");
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
throw new AuthException("Failed to query database!", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handleVanillaLogin(PostLoginEvent loginEvent) {
|
||||
ConnectedPlayer player = (ConnectedPlayer)loginEvent.getPlayer();
|
||||
if(EaglerPipeline.getEaglerHandle(player) == null) {
|
||||
Date lastLogin = null;
|
||||
String lastLoginIP = null;
|
||||
boolean isRegistered = false;
|
||||
synchronized(checkRegistrationByUUID) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
try {
|
||||
String uuidString = getMojangUUID(uuid);
|
||||
checkRegistrationByUUID.setString(1, getMojangUUID(player.getUniqueId()));
|
||||
try(ResultSet res = checkRegistrationByUUID.executeQuery()) {
|
||||
if(res.next()) {
|
||||
isRegistered = true;
|
||||
int vers = res.getInt(1);
|
||||
String username = res.getString(2);
|
||||
lastLogin = res.getDate(3);
|
||||
lastLoginIP = res.getString(4);
|
||||
String playerName = player.getUsername();
|
||||
if(!playerName.equals(username)) {
|
||||
EaglerXVelocity.logger().info(
|
||||
"Player \"{}\" changed their username from \"{}\" to \"{}\", updating authentication database...",
|
||||
uuid.toString(), username, playerName);
|
||||
synchronized(updateMojangUsername) {
|
||||
updateMojangUsername.setString(1, playerName);
|
||||
updateMojangUsername.setString(2, uuidString);
|
||||
if(updateMojangUsername.executeUpdate() == 0) {
|
||||
throw new SQLException("Failed to update username to \"" + playerName + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
byte[] texProperty = getTexturesProperty(player.getGameProfile());
|
||||
if(texProperty != null) {
|
||||
synchronized(updateTextures) {
|
||||
updateTextures.setBytes(1, texProperty);
|
||||
updateTextures.setString(2, uuidString);
|
||||
updateTextures.executeUpdate();
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
EaglerXVelocity.logger().error("Could not look up UUID \"{}\" in auth database!", uuid.toString(), ex);
|
||||
}
|
||||
}
|
||||
if(isRegistered) {
|
||||
if(lastLogin != null) {
|
||||
String dateStr;
|
||||
java.util.Date juLastLogin = new java.util.Date(lastLogin.getTime());
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
int yearToday = calendar.get(Calendar.YEAR);
|
||||
calendar.setTime(juLastLogin);
|
||||
if(calendar.get(Calendar.YEAR) != yearToday) {
|
||||
dateStr = (new SimpleDateFormat("EE, MMM d, yyyy, HH:mm z")).format(juLastLogin);
|
||||
}else {
|
||||
dateStr = (new SimpleDateFormat("EE, MMM d, HH:mm z")).format(juLastLogin);
|
||||
}
|
||||
player.sendMessage(Component.text(
|
||||
lastEaglerLoginMessage.replace("$date", dateStr).replace("$ip", "" + lastLoginIP),
|
||||
NamedTextColor.GREEN));
|
||||
}
|
||||
player.sendMessage(Component.text(useChangeCommandText));
|
||||
}else {
|
||||
player.sendMessage(Component.text(useRegisterCommandText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void destroyStatement(Statement stmt) {
|
||||
try {
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
authLoadingCache.flush();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
destroyStatement(registerUser);
|
||||
destroyStatement(isRegisteredUser);
|
||||
destroyStatement(pruneUsers);
|
||||
destroyStatement(updatePassword);
|
||||
destroyStatement(updateMojangUsername);
|
||||
destroyStatement(getRegistrationsOnIP);
|
||||
destroyStatement(checkRegistrationByUUID);
|
||||
destroyStatement(checkRegistrationByName);
|
||||
destroyStatement(setLastLogin);
|
||||
destroyStatement(updateTextures);
|
||||
try {
|
||||
databaseConnection.close();
|
||||
EaglerXVelocity.logger().info("Successfully disconnected from database '{}'", uri);
|
||||
} catch (SQLException e) {
|
||||
EaglerXVelocity.logger().warn("Exception disconnecting from database '{}'!", uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AuthException extends RuntimeException {
|
||||
|
||||
public AuthException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public AuthException(Throwable t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
public AuthException(String msg, Throwable t) {
|
||||
super(msg, t);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class TooManyRegisteredOnIPException extends AuthException {
|
||||
|
||||
public TooManyRegisteredOnIPException(String ip) {
|
||||
super(ip);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final String hexString = "0123456789abcdef";
|
||||
|
||||
private static final char[] HEX = new char[] {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7',
|
||||
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
|
||||
};
|
||||
|
||||
public static String getMojangUUID(UUID uuid) {
|
||||
char[] ret = new char[32];
|
||||
long msb = uuid.getMostSignificantBits();
|
||||
long lsb = uuid.getLeastSignificantBits();
|
||||
for(int i = 0, j; i < 16; ++i) {
|
||||
j = (15 - i) << 2;
|
||||
ret[i] = HEX[(int)((msb >> j) & 15l)];
|
||||
ret[i + 16] = HEX[(int)((lsb >> j) & 15l)];
|
||||
}
|
||||
return new String(ret);
|
||||
}
|
||||
|
||||
public static UUID parseMojangUUID(String uuid) {
|
||||
long msb = 0l;
|
||||
long lsb = 0l;
|
||||
for(int i = 0, j; i < 16; ++i) {
|
||||
j = (15 - i) << 2;
|
||||
msb |= ((long)hexString.indexOf(uuid.charAt(i)) << j);
|
||||
lsb |= ((long)hexString.indexOf(uuid.charAt(i + 16)) << j);
|
||||
}
|
||||
return new UUID(msb, lsb);
|
||||
}
|
||||
|
||||
private static String sockAddrToString(SocketAddress addr) {
|
||||
if(addr instanceof InetSocketAddress) {
|
||||
return ((InetSocketAddress)addr).getAddress().getHostAddress();
|
||||
}else {
|
||||
return "127.0.0.1";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth;
|
||||
|
||||
/**
|
||||
* base implementation of MD4 family style digest as outlined in "Handbook of
|
||||
* Applied Cryptography", pages 344 - 347.
|
||||
*/
|
||||
public abstract class GeneralDigest {
|
||||
private byte[] xBuf;
|
||||
private int xBufOff;
|
||||
|
||||
private long byteCount;
|
||||
|
||||
/**
|
||||
* Standard constructor
|
||||
*/
|
||||
protected GeneralDigest() {
|
||||
xBuf = new byte[4];
|
||||
xBufOff = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor. We are using copy constructors in place of the
|
||||
* Object.clone() interface as this interface is not supported by J2ME.
|
||||
*/
|
||||
protected GeneralDigest(GeneralDigest t) {
|
||||
xBuf = new byte[t.xBuf.length];
|
||||
System.arraycopy(t.xBuf, 0, xBuf, 0, t.xBuf.length);
|
||||
|
||||
xBufOff = t.xBufOff;
|
||||
byteCount = t.byteCount;
|
||||
}
|
||||
|
||||
public void update(byte in) {
|
||||
xBuf[xBufOff++] = in;
|
||||
|
||||
if (xBufOff == xBuf.length) {
|
||||
processWord(xBuf, 0);
|
||||
xBufOff = 0;
|
||||
}
|
||||
|
||||
byteCount++;
|
||||
}
|
||||
|
||||
public void update(byte[] in, int inOff, int len) {
|
||||
//
|
||||
// fill the current word
|
||||
//
|
||||
while ((xBufOff != 0) && (len > 0)) {
|
||||
update(in[inOff]);
|
||||
|
||||
inOff++;
|
||||
len--;
|
||||
}
|
||||
|
||||
//
|
||||
// process whole words.
|
||||
//
|
||||
while (len > xBuf.length) {
|
||||
processWord(in, inOff);
|
||||
|
||||
inOff += xBuf.length;
|
||||
len -= xBuf.length;
|
||||
byteCount += xBuf.length;
|
||||
}
|
||||
|
||||
//
|
||||
// load in the remainder.
|
||||
//
|
||||
while (len > 0) {
|
||||
update(in[inOff]);
|
||||
|
||||
inOff++;
|
||||
len--;
|
||||
}
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
long bitLength = (byteCount << 3);
|
||||
|
||||
//
|
||||
// add the pad bytes.
|
||||
//
|
||||
update((byte) 128);
|
||||
|
||||
while (xBufOff != 0) {
|
||||
update((byte) 0);
|
||||
}
|
||||
|
||||
processLength(bitLength);
|
||||
|
||||
processBlock();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
byteCount = 0;
|
||||
|
||||
xBufOff = 0;
|
||||
for (int i = 0; i < xBuf.length; i++) {
|
||||
xBuf[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void processWord(byte[] in, int inOff);
|
||||
|
||||
protected abstract void processLength(long bitLength);
|
||||
|
||||
protected abstract void processBlock();
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 HashUtils {
|
||||
|
||||
public static final byte[] EAGLER_SHA256_SALT_BASE = new byte[] { (byte) 117, (byte) 43, (byte) 1, (byte) 112,
|
||||
(byte) 75, (byte) 3, (byte) 188, (byte) 61, (byte) 121, (byte) 31, (byte) 34, (byte) 181, (byte) 234,
|
||||
(byte) 31, (byte) 247, (byte) 72, (byte) 12, (byte) 168, (byte) 138, (byte) 45, (byte) 143, (byte) 77,
|
||||
(byte) 118, (byte) 245, (byte) 187, (byte) 242, (byte) 188, (byte) 219, (byte) 160, (byte) 235, (byte) 235,
|
||||
(byte) 68 };
|
||||
|
||||
public static final byte[] EAGLER_SHA256_SALT_SAVE = new byte[] { (byte) 49, (byte) 25, (byte) 39, (byte) 38,
|
||||
(byte) 253, (byte) 85, (byte) 70, (byte) 245, (byte) 71, (byte) 150, (byte) 253, (byte) 206, (byte) 4,
|
||||
(byte) 26, (byte) 198, (byte) 249, (byte) 145, (byte) 251, (byte) 232, (byte) 174, (byte) 186, (byte) 98,
|
||||
(byte) 27, (byte) 232, (byte) 55, (byte) 144, (byte) 83, (byte) 21, (byte) 36, (byte) 55, (byte) 170,
|
||||
(byte) 118 };
|
||||
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth;
|
||||
|
||||
/**
|
||||
* implementation of SHA-1 as outlined in "Handbook of Applied Cryptography",
|
||||
* pages 346 - 349.
|
||||
*
|
||||
* It is interesting to ponder why the, apart from the extra IV, the other
|
||||
* difference here from MD5 is the "endienness" of the word processing!
|
||||
*/
|
||||
public class SHA1Digest extends GeneralDigest {
|
||||
private static final int DIGEST_LENGTH = 20;
|
||||
|
||||
private int H1, H2, H3, H4, H5;
|
||||
|
||||
private int[] X = new int[80];
|
||||
private int xOff;
|
||||
|
||||
/**
|
||||
* Standard constructor
|
||||
*/
|
||||
public SHA1Digest() {
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor. This will copy the state of the provided message digest.
|
||||
*/
|
||||
public SHA1Digest(SHA1Digest t) {
|
||||
super(t);
|
||||
|
||||
H1 = t.H1;
|
||||
H2 = t.H2;
|
||||
H3 = t.H3;
|
||||
H4 = t.H4;
|
||||
H5 = t.H5;
|
||||
|
||||
System.arraycopy(t.X, 0, X, 0, t.X.length);
|
||||
xOff = t.xOff;
|
||||
}
|
||||
|
||||
public String getAlgorithmName() {
|
||||
return "SHA-1";
|
||||
}
|
||||
|
||||
public int getDigestSize() {
|
||||
return DIGEST_LENGTH;
|
||||
}
|
||||
|
||||
protected void processWord(byte[] in, int inOff) {
|
||||
X[xOff++] = ((in[inOff] & 0xff) << 24) | ((in[inOff + 1] & 0xff) << 16) | ((in[inOff + 2] & 0xff) << 8)
|
||||
| ((in[inOff + 3] & 0xff));
|
||||
|
||||
if (xOff == 16) {
|
||||
processBlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void unpackWord(int word, byte[] out, int outOff) {
|
||||
out[outOff] = (byte) (word >>> 24);
|
||||
out[outOff + 1] = (byte) (word >>> 16);
|
||||
out[outOff + 2] = (byte) (word >>> 8);
|
||||
out[outOff + 3] = (byte) word;
|
||||
}
|
||||
|
||||
protected void processLength(long bitLength) {
|
||||
if (xOff > 14) {
|
||||
processBlock();
|
||||
}
|
||||
|
||||
X[14] = (int) (bitLength >>> 32);
|
||||
X[15] = (int) (bitLength & 0xffffffff);
|
||||
}
|
||||
|
||||
public int doFinal(byte[] out, int outOff) {
|
||||
finish();
|
||||
|
||||
unpackWord(H1, out, outOff);
|
||||
unpackWord(H2, out, outOff + 4);
|
||||
unpackWord(H3, out, outOff + 8);
|
||||
unpackWord(H4, out, outOff + 12);
|
||||
unpackWord(H5, out, outOff + 16);
|
||||
|
||||
reset();
|
||||
|
||||
return DIGEST_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* reset the chaining variables
|
||||
*/
|
||||
public void reset() {
|
||||
super.reset();
|
||||
|
||||
H1 = 0x67452301;
|
||||
H2 = 0xefcdab89;
|
||||
H3 = 0x98badcfe;
|
||||
H4 = 0x10325476;
|
||||
H5 = 0xc3d2e1f0;
|
||||
|
||||
xOff = 0;
|
||||
for (int i = 0; i != X.length; i++) {
|
||||
X[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Additive constants
|
||||
//
|
||||
private static final int Y1 = 0x5a827999;
|
||||
private static final int Y2 = 0x6ed9eba1;
|
||||
private static final int Y3 = 0x8f1bbcdc;
|
||||
private static final int Y4 = 0xca62c1d6;
|
||||
|
||||
private int f(int u, int v, int w) {
|
||||
return ((u & v) | ((~u) & w));
|
||||
}
|
||||
|
||||
private int h(int u, int v, int w) {
|
||||
return (u ^ v ^ w);
|
||||
}
|
||||
|
||||
private int g(int u, int v, int w) {
|
||||
return ((u & v) | (u & w) | (v & w));
|
||||
}
|
||||
|
||||
private int rotateLeft(int x, int n) {
|
||||
return (x << n) | (x >>> (32 - n));
|
||||
}
|
||||
|
||||
protected void processBlock() {
|
||||
//
|
||||
// expand 16 word block into 80 word block.
|
||||
//
|
||||
for (int i = 16; i <= 79; i++) {
|
||||
X[i] = rotateLeft((X[i - 3] ^ X[i - 8] ^ X[i - 14] ^ X[i - 16]), 1);
|
||||
}
|
||||
|
||||
//
|
||||
// set up working variables.
|
||||
//
|
||||
int A = H1;
|
||||
int B = H2;
|
||||
int C = H3;
|
||||
int D = H4;
|
||||
int E = H5;
|
||||
|
||||
//
|
||||
// round 1
|
||||
//
|
||||
for (int j = 0; j <= 19; j++) {
|
||||
int t = rotateLeft(A, 5) + f(B, C, D) + E + X[j] + Y1;
|
||||
|
||||
E = D;
|
||||
D = C;
|
||||
C = rotateLeft(B, 30);
|
||||
B = A;
|
||||
A = t;
|
||||
}
|
||||
|
||||
//
|
||||
// round 2
|
||||
//
|
||||
for (int j = 20; j <= 39; j++) {
|
||||
int t = rotateLeft(A, 5) + h(B, C, D) + E + X[j] + Y2;
|
||||
|
||||
E = D;
|
||||
D = C;
|
||||
C = rotateLeft(B, 30);
|
||||
B = A;
|
||||
A = t;
|
||||
}
|
||||
|
||||
//
|
||||
// round 3
|
||||
//
|
||||
for (int j = 40; j <= 59; j++) {
|
||||
int t = rotateLeft(A, 5) + g(B, C, D) + E + X[j] + Y3;
|
||||
|
||||
E = D;
|
||||
D = C;
|
||||
C = rotateLeft(B, 30);
|
||||
B = A;
|
||||
A = t;
|
||||
}
|
||||
|
||||
//
|
||||
// round 4
|
||||
//
|
||||
for (int j = 60; j <= 79; j++) {
|
||||
int t = rotateLeft(A, 5) + h(B, C, D) + E + X[j] + Y4;
|
||||
|
||||
E = D;
|
||||
D = C;
|
||||
C = rotateLeft(B, 30);
|
||||
B = A;
|
||||
A = t;
|
||||
}
|
||||
|
||||
H1 += A;
|
||||
H2 += B;
|
||||
H3 += C;
|
||||
H4 += D;
|
||||
H5 += E;
|
||||
|
||||
//
|
||||
// reset the offset and clean out the word buffer.
|
||||
//
|
||||
xOff = 0;
|
||||
for (int i = 0; i != X.length; i++) {
|
||||
X[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String hex = "0123456789abcdef";
|
||||
|
||||
public static String hash2string(byte[] b) {
|
||||
char[] ret = new char[b.length * 2];
|
||||
for(int i = 0; i < b.length; ++i) {
|
||||
int bb = (int)b[i] & 0xFF;
|
||||
ret[i * 2] = hex.charAt((bb >> 4) & 0xF);
|
||||
ret[i * 2 + 1] = hex.charAt(bb & 0xF);
|
||||
}
|
||||
return new String(ret);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
* and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth;
|
||||
|
||||
public class SHA256Digest extends GeneralDigest {
|
||||
|
||||
private static final int DIGEST_LENGTH = 32;
|
||||
|
||||
private int H1, H2, H3, H4, H5, H6, H7, H8;
|
||||
|
||||
private int[] X = new int[64];
|
||||
private int xOff;
|
||||
|
||||
public SHA256Digest() {
|
||||
reset();
|
||||
}
|
||||
|
||||
public static int bigEndianToInt(byte[] bs, int off) {
|
||||
int n = bs[off] << 24;
|
||||
n |= (bs[++off] & 0xff) << 16;
|
||||
n |= (bs[++off] & 0xff) << 8;
|
||||
n |= (bs[++off] & 0xff);
|
||||
return n;
|
||||
}
|
||||
|
||||
public static void bigEndianToInt(byte[] bs, int off, int[] ns) {
|
||||
for (int i = 0; i < ns.length; ++i) {
|
||||
ns[i] = bigEndianToInt(bs, off);
|
||||
off += 4;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] intToBigEndian(int n) {
|
||||
byte[] bs = new byte[4];
|
||||
intToBigEndian(n, bs, 0);
|
||||
return bs;
|
||||
}
|
||||
|
||||
public static void intToBigEndian(int n, byte[] bs, int off) {
|
||||
bs[off] = (byte) (n >>> 24);
|
||||
bs[++off] = (byte) (n >>> 16);
|
||||
bs[++off] = (byte) (n >>> 8);
|
||||
bs[++off] = (byte) (n);
|
||||
}
|
||||
|
||||
protected void processWord(byte[] in, int inOff) {
|
||||
X[xOff] = bigEndianToInt(in, inOff);
|
||||
|
||||
if (++xOff == 16) {
|
||||
processBlock();
|
||||
}
|
||||
}
|
||||
|
||||
protected void processLength(long bitLength) {
|
||||
if (xOff > 14) {
|
||||
processBlock();
|
||||
}
|
||||
|
||||
X[14] = (int) (bitLength >>> 32);
|
||||
X[15] = (int) (bitLength & 0xffffffff);
|
||||
}
|
||||
|
||||
public int doFinal(byte[] out, int outOff) {
|
||||
finish();
|
||||
|
||||
intToBigEndian(H1, out, outOff);
|
||||
intToBigEndian(H2, out, outOff + 4);
|
||||
intToBigEndian(H3, out, outOff + 8);
|
||||
intToBigEndian(H4, out, outOff + 12);
|
||||
intToBigEndian(H5, out, outOff + 16);
|
||||
intToBigEndian(H6, out, outOff + 20);
|
||||
intToBigEndian(H7, out, outOff + 24);
|
||||
intToBigEndian(H8, out, outOff + 28);
|
||||
|
||||
reset();
|
||||
|
||||
return DIGEST_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* reset the chaining variables
|
||||
*/
|
||||
public void reset() {
|
||||
super.reset();
|
||||
|
||||
/*
|
||||
* SHA-256 initial hash value The first 32 bits of the fractional parts of the
|
||||
* square roots of the first eight prime numbers
|
||||
*/
|
||||
|
||||
H1 = 0x6a09e667;
|
||||
H2 = 0xbb67ae85;
|
||||
H3 = 0x3c6ef372;
|
||||
H4 = 0xa54ff53a;
|
||||
H5 = 0x510e527f;
|
||||
H6 = 0x9b05688c;
|
||||
H7 = 0x1f83d9ab;
|
||||
H8 = 0x5be0cd19;
|
||||
|
||||
xOff = 0;
|
||||
for (int i = 0; i != X.length; i++) {
|
||||
X[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected void processBlock() {
|
||||
//
|
||||
// expand 16 word block into 64 word blocks.
|
||||
//
|
||||
for (int t = 16; t <= 63; t++) {
|
||||
X[t] = Theta1(X[t - 2]) + X[t - 7] + Theta0(X[t - 15]) + X[t - 16];
|
||||
}
|
||||
|
||||
//
|
||||
// set up working variables.
|
||||
//
|
||||
int a = H1;
|
||||
int b = H2;
|
||||
int c = H3;
|
||||
int d = H4;
|
||||
int e = H5;
|
||||
int f = H6;
|
||||
int g = H7;
|
||||
int h = H8;
|
||||
|
||||
int t = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
// t = 8 * i
|
||||
h += Sum1(e) + Ch(e, f, g) + K[t] + X[t];
|
||||
d += h;
|
||||
h += Sum0(a) + Maj(a, b, c);
|
||||
++t;
|
||||
|
||||
// t = 8 * i + 1
|
||||
g += Sum1(d) + Ch(d, e, f) + K[t] + X[t];
|
||||
c += g;
|
||||
g += Sum0(h) + Maj(h, a, b);
|
||||
++t;
|
||||
|
||||
// t = 8 * i + 2
|
||||
f += Sum1(c) + Ch(c, d, e) + K[t] + X[t];
|
||||
b += f;
|
||||
f += Sum0(g) + Maj(g, h, a);
|
||||
++t;
|
||||
|
||||
// t = 8 * i + 3
|
||||
e += Sum1(b) + Ch(b, c, d) + K[t] + X[t];
|
||||
a += e;
|
||||
e += Sum0(f) + Maj(f, g, h);
|
||||
++t;
|
||||
|
||||
// t = 8 * i + 4
|
||||
d += Sum1(a) + Ch(a, b, c) + K[t] + X[t];
|
||||
h += d;
|
||||
d += Sum0(e) + Maj(e, f, g);
|
||||
++t;
|
||||
|
||||
// t = 8 * i + 5
|
||||
c += Sum1(h) + Ch(h, a, b) + K[t] + X[t];
|
||||
g += c;
|
||||
c += Sum0(d) + Maj(d, e, f);
|
||||
++t;
|
||||
|
||||
// t = 8 * i + 6
|
||||
b += Sum1(g) + Ch(g, h, a) + K[t] + X[t];
|
||||
f += b;
|
||||
b += Sum0(c) + Maj(c, d, e);
|
||||
++t;
|
||||
|
||||
// t = 8 * i + 7
|
||||
a += Sum1(f) + Ch(f, g, h) + K[t] + X[t];
|
||||
e += a;
|
||||
a += Sum0(b) + Maj(b, c, d);
|
||||
++t;
|
||||
}
|
||||
|
||||
H1 += a;
|
||||
H2 += b;
|
||||
H3 += c;
|
||||
H4 += d;
|
||||
H5 += e;
|
||||
H6 += f;
|
||||
H7 += g;
|
||||
H8 += h;
|
||||
|
||||
//
|
||||
// reset the offset and clean out the word buffer.
|
||||
//
|
||||
xOff = 0;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
X[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* SHA-256 functions */
|
||||
private static int Ch(int x, int y, int z) {
|
||||
return (x & y) ^ ((~x) & z);
|
||||
// return z ^ (x & (y ^ z));
|
||||
}
|
||||
|
||||
private static int Maj(int x, int y, int z) {
|
||||
// return (x & y) ^ (x & z) ^ (y & z);
|
||||
return (x & y) | (z & (x ^ y));
|
||||
}
|
||||
|
||||
private static int Sum0(int x) {
|
||||
return ((x >>> 2) | (x << 30)) ^ ((x >>> 13) | (x << 19)) ^ ((x >>> 22) | (x << 10));
|
||||
}
|
||||
|
||||
private static int Sum1(int x) {
|
||||
return ((x >>> 6) | (x << 26)) ^ ((x >>> 11) | (x << 21)) ^ ((x >>> 25) | (x << 7));
|
||||
}
|
||||
|
||||
private static int Theta0(int x) {
|
||||
return ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3);
|
||||
}
|
||||
|
||||
private static int Theta1(int x) {
|
||||
return ((x >>> 17) | (x << 15)) ^ ((x >>> 19) | (x << 13)) ^ (x >>> 10);
|
||||
}
|
||||
|
||||
/*
|
||||
* SHA-256 Constants (represent the first 32 bits of the fractional parts of the
|
||||
* cube roots of the first sixty-four prime numbers)
|
||||
*/
|
||||
static final int K[] = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
|
||||
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152,
|
||||
0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138,
|
||||
0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70,
|
||||
0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
||||
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa,
|
||||
0xa4506ceb, 0xbef9a3f7, 0xc67178f2 };
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth.SHA1Digest;
|
||||
|
||||
/**
|
||||
* 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 CommandConfirmCode extends EaglerCommand {
|
||||
|
||||
public static String confirmHash = null;
|
||||
|
||||
public CommandConfirmCode() {
|
||||
super("confirm-code", "eaglercraft.command.confirmcode", "confirmcode");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSource var1, String[] var2) {
|
||||
if(var2.length != 1) {
|
||||
var1.sendMessage(Component.text("How to use: ", NamedTextColor.RED).append(Component.text("/confirm-code <code>", NamedTextColor.WHITE)));
|
||||
}else {
|
||||
var1.sendMessage(Component.text("Server list 2FA code has been set to: ", NamedTextColor.YELLOW).append(Component.text(var2[0], NamedTextColor.GREEN)));
|
||||
var1.sendMessage(Component.text("You can now return to the server list site and continue", NamedTextColor.YELLOW));
|
||||
byte[] bts = var2[0].getBytes(StandardCharsets.US_ASCII);
|
||||
SHA1Digest dg = new SHA1Digest();
|
||||
dg.update(bts, 0, bts.length);
|
||||
byte[] f = new byte[20];
|
||||
dg.doFinal(f, 0);
|
||||
confirmHash = SHA1Digest.hash2string(f);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPlayerData;
|
||||
|
||||
/**
|
||||
* 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 CommandDomain extends EaglerCommand {
|
||||
|
||||
public CommandDomain() {
|
||||
super("domain", "eaglercraft.command.domain");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSource var1, String[] var2) {
|
||||
if(var2.length != 1) {
|
||||
var1.sendMessage(Component.text("How to use: ", NamedTextColor.RED).append(Component.text("/domain <player>", NamedTextColor.WHITE)));
|
||||
}else {
|
||||
Optional<Player> playerOpt = EaglerXVelocity.proxy().getPlayer(var2[0]);
|
||||
if(playerOpt.isEmpty()) {
|
||||
var1.sendMessage(Component.text("That user is not online", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
EaglerPlayerData eagPlayer = EaglerPipeline.getEaglerHandle(playerOpt.get());
|
||||
if(eagPlayer == null) {
|
||||
var1.sendMessage(Component.text("That user is not using Eaglercraft", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
if(eagPlayer.origin != null) {
|
||||
var1.sendMessage(Component.text("Domain of " + var2[0] + " is '" + eagPlayer.origin + "'", NamedTextColor.BLUE));
|
||||
}else {
|
||||
var1.sendMessage(Component.text("That user's browser did not send an origin header", NamedTextColor.RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.ConsoleCommandSource;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth.DefaultAuthSystem;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth.DefaultAuthSystem.AuthException;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerAuthConfig;
|
||||
|
||||
/**
|
||||
* 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 CommandEaglerPurge extends EaglerCommand {
|
||||
|
||||
public CommandEaglerPurge(String name) {
|
||||
super(name + "-purge", "eaglercraft.command.purge");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSource var1, String[] var2) {
|
||||
if(var1 instanceof ConsoleCommandSource) {
|
||||
if(var2.length != 1) {
|
||||
var1.sendMessage(Component.text("Use /" + name + " <maxAge>", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
int mx;
|
||||
try {
|
||||
mx = Integer.parseInt(var2[0]);
|
||||
}catch(NumberFormatException ex) {
|
||||
var1.sendMessage(Component.text("'" + var2[0] + "' is not an integer!", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
EaglerAuthConfig authConf = EaglerXVelocity.getEagler().getConfig().getAuthConfig();
|
||||
if(authConf.isEnableAuthentication() && authConf.isUseBuiltInAuthentication()) {
|
||||
DefaultAuthSystem srv = EaglerXVelocity.getEagler().getAuthService();
|
||||
if(srv != null) {
|
||||
int cnt;
|
||||
try {
|
||||
EaglerXVelocity.logger().warn("Console is attempting to purge all accounts with {} days of inactivity", mx);
|
||||
cnt = srv.pruneUsers(System.currentTimeMillis() - mx * 86400000l);
|
||||
}catch(AuthException ex) {
|
||||
EaglerXVelocity.logger().error("Failed to purge accounts", ex);
|
||||
var1.sendMessage(Component.text("Failed to purge, check log! Reason: " + ex.getMessage(), NamedTextColor.AQUA));
|
||||
return;
|
||||
}
|
||||
EaglerXVelocity.logger().warn("Console purged {} accounts from auth database", cnt);
|
||||
var1.sendMessage(Component.text("Purged " + cnt + " old accounts from the database", NamedTextColor.AQUA));
|
||||
}
|
||||
}
|
||||
}else {
|
||||
var1.sendMessage(Component.text("This command can only be run from the console!", NamedTextColor.RED));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth.DefaultAuthSystem;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerAuthConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
|
||||
/**
|
||||
* 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 CommandEaglerRegister extends EaglerCommand {
|
||||
|
||||
public CommandEaglerRegister(String name) {
|
||||
super(name, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSource sender, String[] args) {
|
||||
if(sender instanceof ConnectedPlayer) {
|
||||
ConnectedPlayer player = (ConnectedPlayer)sender;
|
||||
if(args.length != 1) {
|
||||
player.sendMessage(Component.text("Use: /" + name + " <password>", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
EaglerAuthConfig authConf = EaglerXVelocity.getEagler().getConfig().getAuthConfig();
|
||||
if(authConf.isEnableAuthentication() && authConf.isUseBuiltInAuthentication()) {
|
||||
DefaultAuthSystem srv = EaglerXVelocity.getEagler().getAuthService();
|
||||
if(srv != null) {
|
||||
if(EaglerPipeline.getEaglerHandle(player) == null) {
|
||||
try {
|
||||
srv.processSetPassword(player, args[0]);
|
||||
sender.sendMessage(Component.text(authConf.getCommandSuccessText()));
|
||||
}catch(DefaultAuthSystem.TooManyRegisteredOnIPException ex) {
|
||||
String tooManyReg = authConf.getTooManyRegistrationsMessage();
|
||||
sender.sendMessage(Component.text(tooManyReg));
|
||||
}catch(DefaultAuthSystem.AuthException ex) {
|
||||
EaglerXVelocity.logger().error("Internal exception while processing password change from \"{}\"", player.getUsername(), ex);
|
||||
sender.sendMessage(Component.text("Internal error, check console logs"));
|
||||
}
|
||||
}else {
|
||||
player.sendMessage(Component.text(authConf.getNeedVanillaToRegisterMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}else {
|
||||
sender.sendMessage(Component.text("You must be a player to use this command!", NamedTextColor.RED));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerVelocityConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerRateLimiter;
|
||||
|
||||
/**
|
||||
* 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 CommandRatelimit extends EaglerCommand {
|
||||
|
||||
public CommandRatelimit() {
|
||||
super("ratelimit", "eaglercraft.command.ratelimit");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSource sender, String[] args) {
|
||||
if((args.length != 1 && args.length != 2) || !args[0].equalsIgnoreCase("reset")) {
|
||||
sender.sendMessage(Component.text("Usage: /ratelimit reset [ip|login|motd|query]", NamedTextColor.RED)); //TODO: allow reset ratelimit on specific listeners
|
||||
}else {
|
||||
int resetNum = 0;
|
||||
if(args.length == 2) {
|
||||
if(args[1].equalsIgnoreCase("ip")) {
|
||||
resetNum = 1;
|
||||
}else if(args[1].equalsIgnoreCase("login")) {
|
||||
resetNum = 2;
|
||||
}else if(args[1].equalsIgnoreCase("motd")) {
|
||||
resetNum = 3;
|
||||
}else if(args[1].equalsIgnoreCase("query")) {
|
||||
resetNum = 4;
|
||||
}else {
|
||||
sender.sendMessage(Component.text("Unknown ratelimit '" + args[1] + "'!", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
}
|
||||
EaglerVelocityConfig conf = EaglerXVelocity.getEagler().getConfig();
|
||||
for(EaglerListenerConfig listener : conf.getServerListeners()) {
|
||||
if(resetNum == 0 || resetNum == 1) {
|
||||
EaglerRateLimiter limiter = listener.getRatelimitIp();
|
||||
if(limiter != null) {
|
||||
limiter.reset();
|
||||
}
|
||||
}
|
||||
if(resetNum == 0 || resetNum == 2) {
|
||||
EaglerRateLimiter limiter = listener.getRatelimitLogin();
|
||||
if(limiter != null) {
|
||||
limiter.reset();
|
||||
}
|
||||
}
|
||||
if(resetNum == 0 || resetNum == 3) {
|
||||
EaglerRateLimiter limiter = listener.getRatelimitMOTD();
|
||||
if(limiter != null) {
|
||||
limiter.reset();
|
||||
}
|
||||
}
|
||||
if(resetNum == 0 || resetNum == 4) {
|
||||
EaglerRateLimiter limiter = listener.getRatelimitQuery();
|
||||
if(limiter != null) {
|
||||
limiter.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
sender.sendMessage(Component.text("Ratelimits reset."));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import com.velocitypowered.proxy.command.VelocityCommandManager;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 abstract class EaglerCommand implements SimpleCommand {
|
||||
|
||||
public final String name;
|
||||
public final String permission;
|
||||
public final String[] alias;
|
||||
|
||||
public EaglerCommand(String name, String perm, String...alias) {
|
||||
this.name = name;
|
||||
this.permission = perm;
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
this.execute(invocation.source(), invocation.arguments());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Invocation invocation) {
|
||||
if(permission != null) {
|
||||
return invocation.source().hasPermission(permission);
|
||||
}else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void execute(CommandSource invocation, String[] args);
|
||||
|
||||
public static void register(EaglerXVelocity plugin, EaglerCommand cmd) {
|
||||
VelocityCommandManager cmdManager = EaglerXVelocity.proxy().getCommandManager();
|
||||
cmdManager.register(cmdManager.metaBuilder(cmd.name).aliases(cmd.alias).plugin(plugin).build(), cmd);
|
||||
}
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.ChatColor;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.Configuration;
|
||||
|
||||
/**
|
||||
* 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 EaglerAuthConfig {
|
||||
|
||||
static EaglerAuthConfig loadConfig(Configuration config) {
|
||||
boolean enableAuthentication = config.getBoolean("enable_authentication_system");
|
||||
boolean useBuiltInAuthentication = config.getBoolean("use_onboard_eaglerx_system");
|
||||
String databaseURI = config.getString("auth_db_uri");
|
||||
String driverClass = config.getString("sql_driver_class", "internal");
|
||||
String driverPath = config.getString("sql_driver_path", null);
|
||||
String passwordPromptScreenText = ChatColor.translateAlternateColorCodes('&', config.getString("password_prompt_screen_text", ""));
|
||||
String notRegisteredScreenText = ChatColor.translateAlternateColorCodes('&', config.getString("not_registered_screen_text", ""));
|
||||
String wrongPasswordScreenText = ChatColor.translateAlternateColorCodes('&', config.getString("wrong_password_screen_text", ""));
|
||||
String eaglerCommandName = config.getString("eagler_command_name");
|
||||
String useRegisterCommandText = ChatColor.translateAlternateColorCodes('&', config.getString("use_register_command_text", ""));
|
||||
String useChangeCommandText = ChatColor.translateAlternateColorCodes('&', config.getString("use_change_command_text", ""));
|
||||
String commandSuccessText = ChatColor.translateAlternateColorCodes('&', config.getString("command_success_text", ""));
|
||||
String lastEaglerLoginMessage = ChatColor.translateAlternateColorCodes('&', config.getString("last_eagler_login_message", ""));
|
||||
String tooManyRegistrationsMessage = ChatColor.translateAlternateColorCodes('&', config.getString("too_many_registrations_message", ""));
|
||||
String needVanillaToRegisterMessage = ChatColor.translateAlternateColorCodes('&', config.getString("need_vanilla_to_register_message", ""));
|
||||
boolean overrideEaglerToVanillaSkins = config.getBoolean("override_eagler_to_vanilla_skins");
|
||||
int maxRegistrationsPerIP = config.getInt("max_registration_per_ip", -1);
|
||||
return new EaglerAuthConfig(enableAuthentication, useBuiltInAuthentication, databaseURI, driverClass,
|
||||
driverPath, passwordPromptScreenText, wrongPasswordScreenText, notRegisteredScreenText,
|
||||
eaglerCommandName, useRegisterCommandText, useChangeCommandText, commandSuccessText,
|
||||
lastEaglerLoginMessage, tooManyRegistrationsMessage, needVanillaToRegisterMessage,
|
||||
overrideEaglerToVanillaSkins, maxRegistrationsPerIP);
|
||||
}
|
||||
|
||||
private boolean enableAuthentication;
|
||||
private boolean useBuiltInAuthentication;
|
||||
|
||||
private final String databaseURI;
|
||||
private final String driverClass;
|
||||
private final String driverPath;
|
||||
private final String passwordPromptScreenText;
|
||||
private final String wrongPasswordScreenText;
|
||||
private final String notRegisteredScreenText;
|
||||
private final String eaglerCommandName;
|
||||
private final String useRegisterCommandText;
|
||||
private final String useChangeCommandText;
|
||||
private final String commandSuccessText;
|
||||
private final String lastEaglerLoginMessage;
|
||||
private final String tooManyRegistrationsMessage;
|
||||
private final String needVanillaToRegisterMessage;
|
||||
private final boolean overrideEaglerToVanillaSkins;
|
||||
private final int maxRegistrationsPerIP;
|
||||
|
||||
private EaglerAuthConfig(boolean enableAuthentication, boolean useBuiltInAuthentication, String databaseURI,
|
||||
String driverClass, String driverPath, String passwordPromptScreenText, String wrongPasswordScreenText,
|
||||
String notRegisteredScreenText, String eaglerCommandName, String useRegisterCommandText,
|
||||
String useChangeCommandText, String commandSuccessText, String lastEaglerLoginMessage,
|
||||
String tooManyRegistrationsMessage, String needVanillaToRegisterMessage,
|
||||
boolean overrideEaglerToVanillaSkins, int maxRegistrationsPerIP) {
|
||||
this.enableAuthentication = enableAuthentication;
|
||||
this.useBuiltInAuthentication = useBuiltInAuthentication;
|
||||
this.databaseURI = databaseURI;
|
||||
this.driverClass = driverClass;
|
||||
this.driverPath = driverPath;
|
||||
this.passwordPromptScreenText = passwordPromptScreenText;
|
||||
this.wrongPasswordScreenText = wrongPasswordScreenText;
|
||||
this.notRegisteredScreenText = notRegisteredScreenText;
|
||||
this.eaglerCommandName = eaglerCommandName;
|
||||
this.useRegisterCommandText = useRegisterCommandText;
|
||||
this.useChangeCommandText = useChangeCommandText;
|
||||
this.commandSuccessText = commandSuccessText;
|
||||
this.lastEaglerLoginMessage = lastEaglerLoginMessage;
|
||||
this.tooManyRegistrationsMessage = tooManyRegistrationsMessage;
|
||||
this.needVanillaToRegisterMessage = needVanillaToRegisterMessage;
|
||||
this.overrideEaglerToVanillaSkins = overrideEaglerToVanillaSkins;
|
||||
this.maxRegistrationsPerIP = maxRegistrationsPerIP;
|
||||
}
|
||||
|
||||
public boolean isEnableAuthentication() {
|
||||
return enableAuthentication;
|
||||
}
|
||||
|
||||
public boolean isUseBuiltInAuthentication() {
|
||||
return useBuiltInAuthentication;
|
||||
}
|
||||
|
||||
public void triggerOnlineModeDisabled() {
|
||||
enableAuthentication = false;
|
||||
useBuiltInAuthentication = false;
|
||||
}
|
||||
|
||||
public String getDatabaseURI() {
|
||||
return databaseURI;
|
||||
}
|
||||
|
||||
public String getDriverClass() {
|
||||
return driverClass;
|
||||
}
|
||||
|
||||
public String getDriverPath() {
|
||||
return driverPath;
|
||||
}
|
||||
|
||||
public String getPasswordPromptScreenText() {
|
||||
return passwordPromptScreenText;
|
||||
}
|
||||
|
||||
public String getWrongPasswordScreenText() {
|
||||
return wrongPasswordScreenText;
|
||||
}
|
||||
|
||||
public String getNotRegisteredScreenText() {
|
||||
return notRegisteredScreenText;
|
||||
}
|
||||
|
||||
public String getEaglerCommandName() {
|
||||
return eaglerCommandName;
|
||||
}
|
||||
|
||||
public String getUseRegisterCommandText() {
|
||||
return useRegisterCommandText;
|
||||
}
|
||||
|
||||
public String getUseChangeCommandText() {
|
||||
return useChangeCommandText;
|
||||
}
|
||||
|
||||
public String getCommandSuccessText() {
|
||||
return commandSuccessText;
|
||||
}
|
||||
|
||||
public String getLastEaglerLoginMessage() {
|
||||
return lastEaglerLoginMessage;
|
||||
}
|
||||
|
||||
public String getTooManyRegistrationsMessage() {
|
||||
return tooManyRegistrationsMessage;
|
||||
}
|
||||
|
||||
public String getNeedVanillaToRegisterMessage() {
|
||||
return needVanillaToRegisterMessage;
|
||||
}
|
||||
|
||||
public boolean getOverrideEaglerToVanillaSkins() {
|
||||
return overrideEaglerToVanillaSkins;
|
||||
}
|
||||
|
||||
public int getMaxRegistrationsPerIP() {
|
||||
return maxRegistrationsPerIP;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,287 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.ChatColor;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.Configuration;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web.HttpContentType;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web.HttpWebServer;
|
||||
|
||||
/**
|
||||
* 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 EaglerListenerConfig {
|
||||
|
||||
static EaglerListenerConfig loadConfig(Configuration config, Map<String, HttpContentType> contentTypes) {
|
||||
|
||||
String host = config.getString("address", "0.0.0.0:8081");
|
||||
InetSocketAddress hostv4 = null;
|
||||
if(host != null && !host.equalsIgnoreCase("null") && !host.equalsIgnoreCase("none")) {
|
||||
int i = host.lastIndexOf(':');
|
||||
if(i == -1) {
|
||||
throw new IllegalArgumentException("Invalid address: " + host + "! Must be an ipv4:port combo");
|
||||
}
|
||||
hostv4 = new InetSocketAddress(host.substring(0, i), Integer.parseInt(host.substring(i + 1)));
|
||||
}
|
||||
|
||||
String hostV6 = config.getString("address_v6", "null");
|
||||
InetSocketAddress hostv6 = null;
|
||||
if(hostV6 != null && !hostV6.equalsIgnoreCase("null") && !hostV6.equalsIgnoreCase("none") && hostV6.length() > 0) {
|
||||
int i = hostV6.lastIndexOf(':');
|
||||
if(i == -1) {
|
||||
throw new IllegalArgumentException("Invalid address: " + host + "! Must be an ipv6:port combo");
|
||||
}
|
||||
hostv6 = new InetSocketAddress(hostV6.substring(0, i), Integer.parseInt(hostV6.substring(i + 1)));
|
||||
}
|
||||
|
||||
if(hostv4 == null && hostv6 == null) {
|
||||
throw new IllegalArgumentException("Invalid host specifies no addresses, both v4 and v6 address are null");
|
||||
}
|
||||
|
||||
int maxPlayer = config.getInt("max_players", 60);
|
||||
boolean forwardIp = config.getBoolean("forward_ip", false);
|
||||
String forwardIpHeader = config.getString("forward_ip_header", "X-Real-IP");
|
||||
String redirectLegacyClientsTo = config.getString("redirect_legacy_clients_to", "null");
|
||||
if(redirectLegacyClientsTo != null && (redirectLegacyClientsTo.equalsIgnoreCase("null") || redirectLegacyClientsTo.length() == 0)) {
|
||||
redirectLegacyClientsTo = null;
|
||||
}
|
||||
String serverIcon = config.getString("server_icon", "server-icon.png");
|
||||
List<String> serverMOTD = (List<String>) config.getList("server_motd", Arrays.asList("&6An EaglercraftX server"));
|
||||
for(int i = 0, l = serverMOTD.size(); i < l; ++i) {
|
||||
serverMOTD.set(i, ChatColor.translateAlternateColorCodes('&', serverMOTD.get(i)));
|
||||
}
|
||||
boolean allowMOTD = config.getBoolean("allow_motd", false);
|
||||
boolean allowQuery = config.getBoolean("allow_query", false);
|
||||
|
||||
int cacheTTL = 7200;
|
||||
boolean cacheAnimation = false;
|
||||
boolean cacheResults = true;
|
||||
boolean cacheTrending = true;
|
||||
boolean cachePortfolios = false;
|
||||
|
||||
Configuration cacheConf = config.getSection("request_motd_cache");
|
||||
if(cacheConf != null) {
|
||||
cacheTTL = cacheConf.getInt("cache_ttl", 7200);
|
||||
cacheAnimation = cacheConf.getBoolean("online_server_list_animation", false);
|
||||
cacheResults = cacheConf.getBoolean("online_server_list_results", true);
|
||||
cacheTrending = cacheConf.getBoolean("online_server_list_trending", true);
|
||||
cachePortfolios = cacheConf.getBoolean("online_server_list_portfolios", false);
|
||||
}
|
||||
|
||||
HttpWebServer httpServer = null;
|
||||
Configuration httpServerConf = config.getSection("http_server");
|
||||
|
||||
if(httpServerConf != null && httpServerConf.getBoolean("enabled", false)) {
|
||||
String rootDirectory = httpServerConf.getString("root", "web");
|
||||
String page404 = httpServerConf.getString("page_404_not_found", "default");
|
||||
if(page404 != null && (page404.length() == 0 || page404.equalsIgnoreCase("null") || page404.equalsIgnoreCase("default"))) {
|
||||
page404 = null;
|
||||
}
|
||||
List<String> defaultIndex = Arrays.asList("index.html", "index.htm");
|
||||
List indexPageRaw = httpServerConf.getList("page_index_name", defaultIndex);
|
||||
List<String> indexPage = new ArrayList(indexPageRaw.size());
|
||||
|
||||
for(int i = 0, l = indexPageRaw.size(); i < l; ++i) {
|
||||
Object o = indexPageRaw.get(i);
|
||||
if(o instanceof String) {
|
||||
indexPage.add((String)o);
|
||||
}
|
||||
}
|
||||
|
||||
if(indexPage.size() == 0) {
|
||||
indexPage.addAll(defaultIndex);
|
||||
}
|
||||
|
||||
httpServer = new HttpWebServer(new File(EaglerXVelocity.getEagler().getDataFolder(), rootDirectory),
|
||||
contentTypes, indexPage, page404);
|
||||
}
|
||||
|
||||
boolean enableVoiceChat = config.getBoolean("allow_voice", false);
|
||||
|
||||
EaglerRateLimiter ratelimitIp = null;
|
||||
EaglerRateLimiter ratelimitLogin = null;
|
||||
EaglerRateLimiter ratelimitMOTD = null;
|
||||
EaglerRateLimiter ratelimitQuery = null;
|
||||
|
||||
Configuration rateLimitConfig = config.getSection("ratelimit");
|
||||
if(rateLimitConfig != null) {
|
||||
Configuration ratelimitIpConfig = rateLimitConfig.getSection("ip");
|
||||
if(ratelimitIpConfig != null && ratelimitIpConfig.getBoolean("enable", false)) {
|
||||
ratelimitIp = EaglerRateLimiter.loadConfig(ratelimitIpConfig);
|
||||
}
|
||||
Configuration ratelimitLoginConfig = rateLimitConfig.getSection("login");
|
||||
if(ratelimitLoginConfig != null && ratelimitLoginConfig.getBoolean("enable", false)) {
|
||||
ratelimitLogin = EaglerRateLimiter.loadConfig(ratelimitLoginConfig);
|
||||
}
|
||||
Configuration ratelimitMOTDConfig = rateLimitConfig.getSection("motd");
|
||||
if(ratelimitMOTDConfig != null && ratelimitMOTDConfig.getBoolean("enable", false)) {
|
||||
ratelimitMOTD = EaglerRateLimiter.loadConfig(ratelimitMOTDConfig);
|
||||
}
|
||||
Configuration ratelimitQueryConfig = rateLimitConfig.getSection("query");
|
||||
if(ratelimitQueryConfig != null && ratelimitQueryConfig.getBoolean("enable", false)) {
|
||||
ratelimitQuery = EaglerRateLimiter.loadConfig(ratelimitQueryConfig);
|
||||
}
|
||||
}
|
||||
|
||||
MOTDCacheConfiguration cacheConfig = new MOTDCacheConfiguration(cacheTTL, cacheAnimation, cacheResults,
|
||||
cacheTrending, cachePortfolios);
|
||||
return new EaglerListenerConfig(hostv4, hostv6, maxPlayer,
|
||||
forwardIp, forwardIpHeader, redirectLegacyClientsTo, serverIcon, serverMOTD, allowMOTD, allowQuery,
|
||||
cacheConfig, httpServer, enableVoiceChat, ratelimitIp, ratelimitLogin, ratelimitMOTD, ratelimitQuery);
|
||||
}
|
||||
|
||||
private final InetSocketAddress address;
|
||||
private final InetSocketAddress addressV6;
|
||||
private final int maxPlayer;
|
||||
private final boolean forwardIp;
|
||||
private final String forwardIpHeader;
|
||||
private final String redirectLegacyClientsTo;
|
||||
private final String serverIcon;
|
||||
private final List<String> serverMOTD;
|
||||
private final boolean allowMOTD;
|
||||
private final boolean allowQuery;
|
||||
private final MOTDCacheConfiguration motdCacheConfig;
|
||||
private final HttpWebServer webServer;
|
||||
private boolean serverIconSet = false;
|
||||
private int[] serverIconPixels = null;
|
||||
private final boolean enableVoiceChat;
|
||||
private final EaglerRateLimiter ratelimitIp;
|
||||
private final EaglerRateLimiter ratelimitLogin;
|
||||
private final EaglerRateLimiter ratelimitMOTD;
|
||||
private final EaglerRateLimiter ratelimitQuery;
|
||||
|
||||
public EaglerListenerConfig(InetSocketAddress address, InetSocketAddress addressV6, int maxPlayer,
|
||||
boolean forwardIp,
|
||||
String forwardIpHeader, String redirectLegacyClientsTo, String serverIcon, List<String> serverMOTD,
|
||||
boolean allowMOTD, boolean allowQuery, MOTDCacheConfiguration motdCacheConfig, HttpWebServer webServer,
|
||||
boolean enableVoiceChat, EaglerRateLimiter ratelimitIp, EaglerRateLimiter ratelimitLogin,
|
||||
EaglerRateLimiter ratelimitMOTD, EaglerRateLimiter ratelimitQuery) {
|
||||
this.address = address;
|
||||
this.addressV6 = addressV6;
|
||||
this.maxPlayer = maxPlayer;
|
||||
this.forwardIp = forwardIp;
|
||||
this.forwardIpHeader = forwardIpHeader;
|
||||
this.redirectLegacyClientsTo = redirectLegacyClientsTo;
|
||||
this.serverIcon = serverIcon;
|
||||
this.serverMOTD = serverMOTD;
|
||||
this.allowMOTD = allowMOTD;
|
||||
this.allowQuery = allowQuery;
|
||||
this.motdCacheConfig = motdCacheConfig;
|
||||
this.webServer = webServer;
|
||||
this.enableVoiceChat = enableVoiceChat;
|
||||
this.ratelimitIp = ratelimitIp;
|
||||
this.ratelimitLogin = ratelimitLogin;
|
||||
this.ratelimitMOTD = ratelimitMOTD;
|
||||
this.ratelimitQuery = ratelimitQuery;
|
||||
}
|
||||
|
||||
public InetSocketAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public InetSocketAddress getAddressV6() {
|
||||
return addressV6;
|
||||
}
|
||||
|
||||
public int getMaxPlayer() {
|
||||
return maxPlayer;
|
||||
}
|
||||
|
||||
public boolean isForwardIp() {
|
||||
return forwardIp;
|
||||
}
|
||||
|
||||
public String getForwardIpHeader() {
|
||||
return forwardIpHeader;
|
||||
}
|
||||
|
||||
public String getServerIconName() {
|
||||
return serverIcon;
|
||||
}
|
||||
|
||||
public int[] getServerIconPixels() {
|
||||
if(!serverIconSet) {
|
||||
if(serverIcon != null) {
|
||||
File f = new File(serverIcon);
|
||||
if(f.isFile()) {
|
||||
serverIconPixels = ServerIconLoader.createServerIcon(f);
|
||||
if(serverIconPixels == null) {
|
||||
EaglerXVelocity.logger().warn("Server icon could not be loaded: {}", f.getAbsolutePath());
|
||||
}
|
||||
}else {
|
||||
EaglerXVelocity.logger().warn("Server icon is not a file: {}", f.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
serverIconSet = true;
|
||||
}
|
||||
return serverIconPixels;
|
||||
}
|
||||
|
||||
public List<String> getServerMOTD() {
|
||||
return serverMOTD;
|
||||
}
|
||||
|
||||
public boolean isAllowMOTD() {
|
||||
return allowMOTD;
|
||||
}
|
||||
|
||||
public boolean isAllowQuery() {
|
||||
return allowQuery;
|
||||
}
|
||||
|
||||
public HttpWebServer getWebServer() {
|
||||
return webServer;
|
||||
}
|
||||
|
||||
public MOTDCacheConfiguration getMOTDCacheConfig() {
|
||||
return motdCacheConfig;
|
||||
}
|
||||
|
||||
public boolean blockRequest(HttpRequest request) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String redirectLegacyClientsTo() {
|
||||
return redirectLegacyClientsTo;
|
||||
}
|
||||
|
||||
public boolean getEnableVoiceChat() {
|
||||
return enableVoiceChat;
|
||||
}
|
||||
|
||||
public EaglerRateLimiter getRatelimitIp() {
|
||||
return ratelimitIp;
|
||||
}
|
||||
|
||||
public EaglerRateLimiter getRatelimitLogin() {
|
||||
return ratelimitLogin;
|
||||
}
|
||||
|
||||
public EaglerRateLimiter getRatelimitMOTD() {
|
||||
return ratelimitMOTD;
|
||||
}
|
||||
|
||||
public EaglerRateLimiter getRatelimitQuery() {
|
||||
return ratelimitQuery;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.Configuration;
|
||||
|
||||
/**
|
||||
* 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 EaglerRateLimiter {
|
||||
|
||||
private final int period;
|
||||
private final int limit;
|
||||
private final int limitLockout;
|
||||
private int effectiveLimit;
|
||||
private int effectiveLimitLockout;
|
||||
private final int lockoutDuration;
|
||||
private final List<String> exceptions;
|
||||
|
||||
private EaglerRateLimiter(int period, int limit, int limitLockout, int lockoutDuration, List<String> exceptions) {
|
||||
this.period = period * 1000 / limit;
|
||||
this.limit = this.effectiveLimit = limit;
|
||||
this.limitLockout = this.effectiveLimitLockout = limitLockout;
|
||||
this.lockoutDuration = lockoutDuration * 1000;
|
||||
this.exceptions = exceptions;
|
||||
}
|
||||
|
||||
public void setDivisor(int d) {
|
||||
this.effectiveLimit = this.limit * d;
|
||||
this.effectiveLimitLockout = this.limitLockout * d;
|
||||
}
|
||||
|
||||
public int getPeriod() {
|
||||
return period;
|
||||
}
|
||||
|
||||
public int getLimit() {
|
||||
return effectiveLimit;
|
||||
}
|
||||
|
||||
public int getLimitLockout() {
|
||||
return effectiveLimitLockout;
|
||||
}
|
||||
|
||||
public int getLockoutDuration() {
|
||||
return lockoutDuration;
|
||||
}
|
||||
|
||||
public List<String> getExceptions() {
|
||||
return exceptions;
|
||||
}
|
||||
|
||||
public boolean isException(String addr) {
|
||||
for(int i = 0, l = exceptions.size(); i < l; ++i) {
|
||||
String str = exceptions.get(i);
|
||||
int ll = str.length() - 1;
|
||||
if(str.indexOf('*') == 0) {
|
||||
if(addr.endsWith(str.substring(1))) {
|
||||
return true;
|
||||
}
|
||||
}else if(str.lastIndexOf('*') == ll) {
|
||||
if(addr.startsWith(str.substring(ll))) {
|
||||
return true;
|
||||
}
|
||||
}else {
|
||||
if(addr.equals(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected class RateLimiter {
|
||||
|
||||
protected int requestCounter = 0;
|
||||
protected long lockoutTimestamp = 0l;
|
||||
protected long cooldownTimestamp = 0l;
|
||||
|
||||
protected RateLimitStatus rateLimit() {
|
||||
long millis = System.currentTimeMillis();
|
||||
tick(millis);
|
||||
if(lockoutTimestamp != 0l) {
|
||||
return RateLimitStatus.LOCKED_OUT;
|
||||
}else {
|
||||
if(++requestCounter > EaglerRateLimiter.this.effectiveLimitLockout) {
|
||||
lockoutTimestamp = millis;
|
||||
requestCounter = 0;
|
||||
return RateLimitStatus.LIMITED_NOW_LOCKED_OUT;
|
||||
}else if(requestCounter > EaglerRateLimiter.this.effectiveLimit) {
|
||||
return RateLimitStatus.LIMITED;
|
||||
}else {
|
||||
return RateLimitStatus.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void tick(long millis) {
|
||||
if(lockoutTimestamp != 0l) {
|
||||
if(millis - lockoutTimestamp > EaglerRateLimiter.this.lockoutDuration) {
|
||||
requestCounter = 0;
|
||||
lockoutTimestamp = 0l;
|
||||
cooldownTimestamp = millis;
|
||||
}
|
||||
}else {
|
||||
long delta = millis - cooldownTimestamp;
|
||||
long decr = delta / EaglerRateLimiter.this.period;
|
||||
if(decr >= requestCounter) {
|
||||
requestCounter = 0;
|
||||
cooldownTimestamp = millis;
|
||||
}else {
|
||||
requestCounter -= decr;
|
||||
cooldownTimestamp += decr * EaglerRateLimiter.this.period;
|
||||
if(requestCounter < 0) {
|
||||
requestCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<String, RateLimiter> ratelimiters = new HashMap();
|
||||
|
||||
public RateLimitStatus rateLimit(String addr) {
|
||||
addr = addr.toLowerCase();
|
||||
if(isException(addr)) {
|
||||
return RateLimitStatus.OK;
|
||||
}else {
|
||||
RateLimiter limiter;
|
||||
synchronized(ratelimiters) {
|
||||
limiter = ratelimiters.get(addr);
|
||||
if(limiter == null) {
|
||||
limiter = new RateLimiter();
|
||||
ratelimiters.put(addr, limiter);
|
||||
}
|
||||
}
|
||||
return limiter.rateLimit();
|
||||
}
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
long millis = System.currentTimeMillis();
|
||||
synchronized(ratelimiters) {
|
||||
Iterator<RateLimiter> itr = ratelimiters.values().iterator();
|
||||
while(itr.hasNext()) {
|
||||
RateLimiter i = itr.next();
|
||||
i.tick(millis);
|
||||
if(i.requestCounter <= 0 && i.lockoutTimestamp <= 0l) {
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
synchronized(ratelimiters) {
|
||||
ratelimiters.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static EaglerRateLimiter loadConfig(Configuration config) {
|
||||
int period = config.getInt("period", -1);
|
||||
int limit = config.getInt("limit", -1);
|
||||
int limitLockout = config.getInt("limit_lockout", -1);
|
||||
int lockoutDuration = config.getInt("lockout_duration", -1);
|
||||
Collection<String> exc = (Collection<String>) config.getList("exceptions");
|
||||
List<String> exceptions = new ArrayList();
|
||||
for(String str : exc) {
|
||||
exceptions.add(str.toLowerCase());
|
||||
}
|
||||
if(period != -1 && limit != -1 && limitLockout != -1 && lockoutDuration != -1) {
|
||||
return new EaglerRateLimiter(period, limit, limitLockout, lockoutDuration, exceptions);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.Configuration;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 EaglerUpdateConfig {
|
||||
|
||||
static EaglerUpdateConfig loadConfig(Configuration config) {
|
||||
boolean blockAllClientUpdates = config.getBoolean("block_all_client_updates", false);
|
||||
boolean discardLoginPacketCerts = config.getBoolean("discard_login_packet_certs", false);
|
||||
int certPacketDataRateLimit = config.getInt("cert_packet_data_rate_limit", 524288);
|
||||
boolean enableEagcertFolder = config.getBoolean("enable_eagcert_folder", true);
|
||||
boolean downloadLatestCerts = config.getBoolean("download_latest_certs", true);
|
||||
int checkForUpdatesEvery = config.getInt("check_for_update_every", 900);
|
||||
Collection<String> downloadCertURLs = (Collection<String>)config.getList("download_certs_from");
|
||||
return new EaglerUpdateConfig(blockAllClientUpdates, discardLoginPacketCerts, certPacketDataRateLimit,
|
||||
enableEagcertFolder, downloadLatestCerts, checkForUpdatesEvery, downloadCertURLs);
|
||||
}
|
||||
|
||||
private final boolean blockAllClientUpdates;
|
||||
private final boolean discardLoginPacketCerts;
|
||||
private final int certPacketDataRateLimit;
|
||||
private final boolean enableEagcertFolder;
|
||||
private final boolean downloadLatestCerts;
|
||||
private final int checkForUpdatesEvery;
|
||||
private final Collection<String> downloadCertURLs;
|
||||
|
||||
public EaglerUpdateConfig(boolean blockAllClientUpdates, boolean discardLoginPacketCerts,
|
||||
int certPacketDataRateLimit, boolean enableEagcertFolder, boolean downloadLatestCerts,
|
||||
int checkForUpdatesEvery, Collection<String> downloadCertURLs) {
|
||||
this.blockAllClientUpdates = blockAllClientUpdates;
|
||||
this.discardLoginPacketCerts = discardLoginPacketCerts;
|
||||
this.certPacketDataRateLimit = certPacketDataRateLimit;
|
||||
this.enableEagcertFolder = enableEagcertFolder;
|
||||
this.downloadLatestCerts = downloadLatestCerts;
|
||||
this.checkForUpdatesEvery = checkForUpdatesEvery;
|
||||
this.downloadCertURLs = downloadCertURLs;
|
||||
}
|
||||
|
||||
public boolean isBlockAllClientUpdates() {
|
||||
return blockAllClientUpdates;
|
||||
}
|
||||
|
||||
public boolean isDiscardLoginPacketCerts() {
|
||||
return discardLoginPacketCerts;
|
||||
}
|
||||
|
||||
public int getCertPacketDataRateLimit() {
|
||||
return certPacketDataRateLimit;
|
||||
}
|
||||
|
||||
public boolean isEnableEagcertFolder() {
|
||||
return enableEagcertFolder;
|
||||
}
|
||||
|
||||
public boolean isDownloadLatestCerts() {
|
||||
return downloadLatestCerts && enableEagcertFolder;
|
||||
}
|
||||
|
||||
public int getCheckForUpdatesEvery() {
|
||||
return checkForUpdatesEvery;
|
||||
}
|
||||
|
||||
public Collection<String> getDownloadCertURLs() {
|
||||
return downloadCertURLs;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,478 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.velocitypowered.api.util.GameProfile.Property;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.Configuration;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.ConfigurationProvider;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee.YamlConfiguration;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web.HttpContentType;
|
||||
|
||||
/**
|
||||
* 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 EaglerVelocityConfig {
|
||||
|
||||
public static EaglerVelocityConfig loadConfig(File directory) throws IOException {
|
||||
Map<String, HttpContentType> contentTypes = new HashMap();
|
||||
|
||||
try(InputStream is = new FileInputStream(getConfigFile(directory, "http_mime_types.json"))) {
|
||||
loadMimeTypes(is, contentTypes);
|
||||
}catch(Throwable t) {
|
||||
try(InputStream is = EaglerVelocityConfig.class.getResourceAsStream("default_http_mime_types.json")) {
|
||||
loadMimeTypes(is, contentTypes);
|
||||
}catch(IOException ex) {
|
||||
EaglerXVelocity.logger().error("Could not load default_http_mime_types.json!");
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
directory.mkdirs();
|
||||
ConfigurationProvider prov = ConfigurationProvider.getProvider(YamlConfiguration.class);
|
||||
|
||||
Configuration configYml = prov.load(getConfigFile(directory, "settings.yml"));
|
||||
String serverName = configYml.getString("server_name", "EaglercraftXVelocity Server");
|
||||
String serverUUIDString = configYml.getString("server_uuid", null);
|
||||
if(serverUUIDString == null) {
|
||||
throw new IOException("You must specify a server_uuid!");
|
||||
}
|
||||
|
||||
UUID serverUUID = null;
|
||||
try {
|
||||
serverUUID = UUID.fromString(serverUUIDString);
|
||||
}catch(Throwable t) {
|
||||
}
|
||||
|
||||
if(serverUUID == null) {
|
||||
throw new IOException("The server_uuid \"" + serverUUIDString + "\" is invalid!");
|
||||
}
|
||||
|
||||
Configuration listenerYml = prov.load(getConfigFile(directory, "listeners.yml"));
|
||||
Iterator<String> listeners = listenerYml.getKeys().iterator();
|
||||
Map<String, EaglerListenerConfig> serverListeners = new HashMap();
|
||||
boolean voiceChat = false;
|
||||
|
||||
while(listeners.hasNext()) {
|
||||
String name = listeners.next();
|
||||
EaglerListenerConfig conf = EaglerListenerConfig.loadConfig(listenerYml.getSection(name), contentTypes);
|
||||
if(conf != null) {
|
||||
serverListeners.put(name, conf);
|
||||
voiceChat |= conf.getEnableVoiceChat();
|
||||
}else {
|
||||
EaglerXVelocity.logger().error("Invalid listener config: {}", name);
|
||||
}
|
||||
}
|
||||
|
||||
if(serverListeners.size() == 0) {
|
||||
EaglerXVelocity.logger().error("No Listeners Configured!");
|
||||
}
|
||||
|
||||
Configuration authserivceYml = prov.load(getConfigFile(directory, "authservice.yml"));
|
||||
EaglerAuthConfig authConfig = EaglerAuthConfig.loadConfig(authserivceYml);
|
||||
|
||||
Configuration updatesYml = prov.load(getConfigFile(directory, "updates.yml"));
|
||||
EaglerUpdateConfig updatesConfig = EaglerUpdateConfig.loadConfig(updatesYml);
|
||||
|
||||
Configuration iceServersYml = prov.load(getConfigFile(directory, "ice_servers.yml"));
|
||||
Collection<String> iceServers = loadICEServers(iceServersYml);
|
||||
|
||||
if(authConfig.isEnableAuthentication()) {
|
||||
for(EaglerListenerConfig lst : serverListeners.values()) {
|
||||
if(lst.getRatelimitLogin() != null) lst.getRatelimitLogin().setDivisor(2);
|
||||
if(lst.getRatelimitIp() != null) lst.getRatelimitIp().setDivisor(2);
|
||||
}
|
||||
}
|
||||
|
||||
long websocketKeepAliveTimeout = configYml.getInt("websocket_connection_timeout", 15000);
|
||||
long websocketHandshakeTimeout = configYml.getInt("websocket_handshake_timeout", 5000);
|
||||
int websocketCompressionLevel = configYml.getInt("http_websocket_compression_level", 6);
|
||||
|
||||
boolean downloadVanillaSkins = configYml.getBoolean("download_vanilla_skins_to_clients", false);
|
||||
Collection<String> validSkinUrls = (Collection<String>)configYml.getList("valid_skin_download_urls");
|
||||
int uuidRateLimitPlayer = configYml.getInt("uuid_lookup_ratelimit_player", 50);
|
||||
int uuidRateLimitGlobal = configYml.getInt("uuid_lookup_ratelimit_global", 175);
|
||||
int skinRateLimitPlayer = configYml.getInt("skin_download_ratelimit_player", 1000);
|
||||
int skinRateLimitGlobal = configYml.getInt("skin_download_ratelimit_global", 30000);
|
||||
|
||||
String skinCacheURI = configYml.getString("skin_cache_db_uri", "jdbc:sqlite:eaglercraft_skins_cache.db");
|
||||
int keepObjectsDays = configYml.getInt("skin_cache_keep_objects_days", 45);
|
||||
int keepProfilesDays = configYml.getInt("skin_cache_keep_profiles_days", 7);
|
||||
int maxObjects = configYml.getInt("skin_cache_max_objects", 32768);
|
||||
int maxProfiles = configYml.getInt("skin_cache_max_profiles", 32768);
|
||||
int antagonistsRateLimit = configYml.getInt("skin_cache_antagonists_ratelimit", 15);
|
||||
String sqliteDriverClass = configYml.getString("sql_driver_class", "internal");
|
||||
String sqliteDriverPath = configYml.getString("sql_driver_path", null);
|
||||
String eaglerPlayersVanillaSkin = configYml.getString("eagler_players_vanilla_skin", null);
|
||||
if(eaglerPlayersVanillaSkin != null && eaglerPlayersVanillaSkin.length() == 0) {
|
||||
eaglerPlayersVanillaSkin = null;
|
||||
}
|
||||
boolean enableIsEaglerPlayerProperty = configYml.getBoolean("enable_is_eagler_player_property", true);
|
||||
Set<String> disableVoiceOnServers = new HashSet((Collection<String>)configYml.getList("disable_voice_chat_on_servers"));
|
||||
boolean disableFNAWSkinsEverywhere = configYml.getBoolean("disable_fnaw_skins_everywhere", false);
|
||||
Set<String> disableFNAWSkinsOnServers = new HashSet((Collection<String>)configYml.getList("disable_fnaw_skins_on_servers"));
|
||||
|
||||
final EaglerVelocityConfig ret = new EaglerVelocityConfig(serverName, serverUUID, websocketKeepAliveTimeout,
|
||||
websocketHandshakeTimeout, websocketCompressionLevel, serverListeners, contentTypes,
|
||||
downloadVanillaSkins, validSkinUrls, uuidRateLimitPlayer, uuidRateLimitGlobal, skinRateLimitPlayer,
|
||||
skinRateLimitGlobal, skinCacheURI, keepObjectsDays, keepProfilesDays, maxObjects, maxProfiles,
|
||||
antagonistsRateLimit, sqliteDriverClass, sqliteDriverPath, eaglerPlayersVanillaSkin,
|
||||
enableIsEaglerPlayerProperty, authConfig, updatesConfig, iceServers, voiceChat,
|
||||
disableVoiceOnServers, disableFNAWSkinsEverywhere, disableFNAWSkinsOnServers);
|
||||
|
||||
if(eaglerPlayersVanillaSkin != null) {
|
||||
VanillaDefaultSkinProfileLoader.lookupVanillaSkinUser(ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static File getConfigFile(File directory, String fileName) throws IOException {
|
||||
File file = new File(directory, fileName);
|
||||
if(!file.isFile()) {
|
||||
try (BufferedReader is = new BufferedReader(new InputStreamReader(
|
||||
EaglerVelocityConfig.class.getResourceAsStream("default_" + fileName), StandardCharsets.UTF_8));
|
||||
PrintWriter os = new PrintWriter(new FileWriter(file))) {
|
||||
String line;
|
||||
while((line = is.readLine()) != null) {
|
||||
if(line.contains("${")) {
|
||||
line = line.replace("${random_uuid}", UUID.randomUUID().toString());
|
||||
}
|
||||
os.println(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private static void loadMimeTypes(InputStream file, Map<String, HttpContentType> contentTypes) throws IOException {
|
||||
JsonObject obj = parseJsonObject(file);
|
||||
for(Entry<String, JsonElement> etr : obj.entrySet()) {
|
||||
String mime = etr.getKey();
|
||||
try {
|
||||
JsonObject e = etr.getValue().getAsJsonObject();
|
||||
JsonArray arr = e.getAsJsonArray("files");
|
||||
if(arr == null || arr.size() == 0) {
|
||||
EaglerXVelocity.logger().warn("MIME type '{}' defines no extensions!", mime);
|
||||
continue;
|
||||
}
|
||||
HashSet<String> exts = new HashSet();
|
||||
for(int i = 0, l = arr.size(); i < l; ++i) {
|
||||
exts.add(arr.get(i).getAsString());
|
||||
}
|
||||
long expires = 0l;
|
||||
JsonElement ex = e.get("expires");
|
||||
if(ex != null) {
|
||||
expires = ex.getAsInt() * 1000l;
|
||||
}
|
||||
String charset = null;
|
||||
ex = e.get("charset");
|
||||
if(ex != null) {
|
||||
charset = ex.getAsString();
|
||||
}
|
||||
HttpContentType typeObj = new HttpContentType(exts, mime, charset, expires);
|
||||
for(String s : exts) {
|
||||
contentTypes.put(s, typeObj);
|
||||
}
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().warn("Exception parsing MIME type '{}' - {}", mime, t.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Collection<String> loadICEServers(Configuration config) {
|
||||
Collection<String> ret = new ArrayList(config.getList("voice_stun_servers"));
|
||||
Configuration turnServers = config.getSection("voice_turn_servers");
|
||||
Iterator<String> turnItr = turnServers.getKeys().iterator();
|
||||
while(turnItr.hasNext()) {
|
||||
String name = turnItr.next();
|
||||
Configuration turnSvr = turnServers.getSection(name);
|
||||
ret.add(turnSvr.getString("url") + ";" + turnSvr.getString("username") + ";" + turnSvr.getString("password"));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static JsonObject parseJsonObject(InputStream file) throws IOException {
|
||||
StringBuilder str = new StringBuilder();
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
int i;
|
||||
while((i = file.read(buffer)) > 0) {
|
||||
str.append(new String(buffer, 0, i, "UTF-8"));
|
||||
}
|
||||
|
||||
try {
|
||||
return JsonParser.parseString(str.toString()).getAsJsonObject();
|
||||
}catch(JsonSyntaxException ex) {
|
||||
throw new IOException("Invalid JSONObject", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static final Property isEaglerProperty = new Property("isEaglerPlayer", "true", ""); //TODO: how to have null signature?
|
||||
|
||||
private final String serverName;
|
||||
private final UUID serverUUID;
|
||||
private final long websocketKeepAliveTimeout;
|
||||
private final long websocketHandshakeTimeout;
|
||||
private final int httpWebsocketCompressionLevel;
|
||||
private final Map<String, EaglerListenerConfig> serverListeners;
|
||||
private final Map<String, HttpContentType> contentTypes;
|
||||
private final boolean downloadVanillaSkins;
|
||||
private final Collection<String> validSkinUrls;
|
||||
private final int uuidRateLimitPlayer;
|
||||
private final int uuidRateLimitGlobal;
|
||||
private final int skinRateLimitPlayer;
|
||||
private final int skinRateLimitGlobal;
|
||||
private final String skinCacheURI;
|
||||
private final int keepObjectsDays;
|
||||
private final int keepProfilesDays;
|
||||
private final int maxObjects;
|
||||
private final int maxProfiles;
|
||||
private final int antagonistsRateLimit;
|
||||
private final String sqliteDriverClass;
|
||||
private final String sqliteDriverPath;
|
||||
private final String eaglerPlayersVanillaSkin;
|
||||
private final boolean enableIsEaglerPlayerProperty;
|
||||
private final EaglerAuthConfig authConfig;
|
||||
private final EaglerUpdateConfig updateConfig;
|
||||
private final Collection<String> iceServers;
|
||||
private final boolean enableVoiceChat;
|
||||
private final Set<String> disableVoiceOnServers;
|
||||
private final boolean disableFNAWSkinsEverywhere;
|
||||
private final Set<String> disableFNAWSkinsOnServers;
|
||||
Property[] eaglerPlayersVanillaSkinCached = new Property[] { isEaglerProperty };
|
||||
|
||||
public String getServerName() {
|
||||
return serverName;
|
||||
}
|
||||
|
||||
public UUID getServerUUID() {
|
||||
return serverUUID;
|
||||
}
|
||||
|
||||
public long getWebsocketKeepAliveTimeout() {
|
||||
return websocketKeepAliveTimeout;
|
||||
}
|
||||
|
||||
public long getWebsocketHandshakeTimeout() {
|
||||
return websocketHandshakeTimeout;
|
||||
}
|
||||
|
||||
public int getHttpWebsocketCompressionLevel() {
|
||||
return httpWebsocketCompressionLevel;
|
||||
}
|
||||
|
||||
public Collection<EaglerListenerConfig> getServerListeners() {
|
||||
return serverListeners.values();
|
||||
}
|
||||
|
||||
public EaglerListenerConfig getServerListenersByName(String name) {
|
||||
return serverListeners.get(name);
|
||||
}
|
||||
|
||||
public Map<String, HttpContentType> getContentType() {
|
||||
return contentTypes;
|
||||
}
|
||||
|
||||
public Map<String, HttpContentType> getContentTypes() {
|
||||
return contentTypes;
|
||||
}
|
||||
|
||||
public boolean getDownloadVanillaSkins() {
|
||||
return downloadVanillaSkins;
|
||||
}
|
||||
|
||||
public Collection<String> getValidSkinUrls() {
|
||||
return validSkinUrls;
|
||||
}
|
||||
|
||||
public boolean isValidSkinHost(String host) {
|
||||
host = host.toLowerCase();
|
||||
for(String str : validSkinUrls) {
|
||||
if(str.length() > 0) {
|
||||
str = str.toLowerCase();
|
||||
if(str.charAt(0) == '*') {
|
||||
if(host.endsWith(str.substring(1))) {
|
||||
return true;
|
||||
}
|
||||
}else {
|
||||
if(host.equals(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getUuidRateLimitPlayer() {
|
||||
return uuidRateLimitPlayer;
|
||||
}
|
||||
|
||||
public int getUuidRateLimitGlobal() {
|
||||
return uuidRateLimitGlobal;
|
||||
}
|
||||
|
||||
public int getSkinRateLimitPlayer() {
|
||||
return skinRateLimitPlayer;
|
||||
}
|
||||
|
||||
public int getSkinRateLimitGlobal() {
|
||||
return skinRateLimitGlobal;
|
||||
}
|
||||
|
||||
public String getSkinCacheURI() {
|
||||
return skinCacheURI;
|
||||
}
|
||||
|
||||
public String getSQLiteDriverClass() {
|
||||
return sqliteDriverClass;
|
||||
}
|
||||
|
||||
public String getSQLiteDriverPath() {
|
||||
return sqliteDriverPath;
|
||||
}
|
||||
|
||||
public int getKeepObjectsDays() {
|
||||
return keepObjectsDays;
|
||||
}
|
||||
|
||||
public int getKeepProfilesDays() {
|
||||
return keepProfilesDays;
|
||||
}
|
||||
|
||||
public int getMaxObjects() {
|
||||
return maxObjects;
|
||||
}
|
||||
|
||||
public int getMaxProfiles() {
|
||||
return maxProfiles;
|
||||
}
|
||||
|
||||
public int getAntagonistsRateLimit() {
|
||||
return antagonistsRateLimit;
|
||||
}
|
||||
|
||||
public String getEaglerPlayersVanillaSkin() {
|
||||
return eaglerPlayersVanillaSkin;
|
||||
}
|
||||
|
||||
public boolean getEnableIsEaglerPlayerProperty() {
|
||||
return enableIsEaglerPlayerProperty;
|
||||
}
|
||||
|
||||
public Property[] getEaglerPlayersVanillaSkinProperties() {
|
||||
return eaglerPlayersVanillaSkinCached;
|
||||
}
|
||||
|
||||
public boolean isCracked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public EaglerAuthConfig getAuthConfig() {
|
||||
return authConfig;
|
||||
}
|
||||
|
||||
public EaglerUpdateConfig getUpdateConfig() {
|
||||
return updateConfig;
|
||||
}
|
||||
|
||||
public Collection<String> getICEServers() {
|
||||
return iceServers;
|
||||
}
|
||||
|
||||
public boolean getEnableVoiceChat() {
|
||||
return enableVoiceChat;
|
||||
}
|
||||
|
||||
public Set<String> getDisableVoiceOnServersSet() {
|
||||
return disableVoiceOnServers;
|
||||
}
|
||||
|
||||
public boolean getDisableFNAWSkinsEverywhere() {
|
||||
return disableFNAWSkinsEverywhere;
|
||||
}
|
||||
|
||||
public Set<String> getDisableFNAWSkinsOnServersSet() {
|
||||
return disableFNAWSkinsOnServers;
|
||||
}
|
||||
|
||||
private EaglerVelocityConfig(String serverName, UUID serverUUID, long websocketKeepAliveTimeout,
|
||||
long websocketHandshakeTimeout, int httpWebsocketCompressionLevel,
|
||||
Map<String, EaglerListenerConfig> serverListeners, Map<String, HttpContentType> contentTypes,
|
||||
boolean downloadVanillaSkins, Collection<String> validSkinUrls, int uuidRateLimitPlayer,
|
||||
int uuidRateLimitGlobal, int skinRateLimitPlayer, int skinRateLimitGlobal, String skinCacheURI,
|
||||
int keepObjectsDays, int keepProfilesDays, int maxObjects, int maxProfiles, int antagonistsRateLimit,
|
||||
String sqliteDriverClass, String sqliteDriverPath, String eaglerPlayersVanillaSkin,
|
||||
boolean enableIsEaglerPlayerProperty, EaglerAuthConfig authConfig, EaglerUpdateConfig updateConfig,
|
||||
Collection<String> iceServers, boolean enableVoiceChat, Set<String> disableVoiceOnServers,
|
||||
boolean disableFNAWSkinsEverywhere, Set<String> disableFNAWSkinsOnServers) {
|
||||
this.serverName = serverName;
|
||||
this.serverUUID = serverUUID;
|
||||
this.serverListeners = serverListeners;
|
||||
this.websocketHandshakeTimeout = websocketHandshakeTimeout;
|
||||
this.websocketKeepAliveTimeout = websocketKeepAliveTimeout;
|
||||
this.httpWebsocketCompressionLevel = httpWebsocketCompressionLevel;
|
||||
this.contentTypes = contentTypes;
|
||||
this.downloadVanillaSkins = downloadVanillaSkins;
|
||||
this.validSkinUrls = validSkinUrls;
|
||||
this.uuidRateLimitPlayer = uuidRateLimitPlayer;
|
||||
this.uuidRateLimitGlobal = uuidRateLimitGlobal;
|
||||
this.skinRateLimitPlayer = skinRateLimitPlayer;
|
||||
this.skinRateLimitGlobal = skinRateLimitGlobal;
|
||||
this.skinCacheURI = skinCacheURI;
|
||||
this.keepObjectsDays = keepObjectsDays;
|
||||
this.keepProfilesDays = keepProfilesDays;
|
||||
this.maxObjects = maxObjects;
|
||||
this.maxProfiles = maxProfiles;
|
||||
this.antagonistsRateLimit = antagonistsRateLimit;
|
||||
this.sqliteDriverClass = sqliteDriverClass;
|
||||
this.sqliteDriverPath = sqliteDriverPath;
|
||||
this.eaglerPlayersVanillaSkin = eaglerPlayersVanillaSkin;
|
||||
this.enableIsEaglerPlayerProperty = enableIsEaglerPlayerProperty;
|
||||
this.authConfig = authConfig;
|
||||
this.updateConfig = updateConfig;
|
||||
this.iceServers = iceServers;
|
||||
this.enableVoiceChat = enableVoiceChat;
|
||||
this.disableVoiceOnServers = disableVoiceOnServers;
|
||||
this.disableFNAWSkinsEverywhere = disableFNAWSkinsEverywhere;
|
||||
this.disableFNAWSkinsOnServers = disableFNAWSkinsOnServers;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 MOTDCacheConfiguration {
|
||||
|
||||
public final int cacheTTL;
|
||||
public final boolean cacheServerListAnimation;
|
||||
public final boolean cacheServerListResults;
|
||||
public final boolean cacheServerListTrending;
|
||||
public final boolean cacheServerListPortfolios;
|
||||
|
||||
public MOTDCacheConfiguration(int cacheTTL, boolean cacheServerListAnimation, boolean cacheServerListResults,
|
||||
boolean cacheServerListTrending, boolean cacheServerListPortfolios) {
|
||||
this.cacheTTL = cacheTTL;
|
||||
this.cacheServerListAnimation = cacheServerListAnimation;
|
||||
this.cacheServerListResults = cacheServerListResults;
|
||||
this.cacheServerListTrending = cacheServerListTrending;
|
||||
this.cacheServerListPortfolios = cacheServerListPortfolios;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 enum RateLimitStatus {
|
||||
OK, LIMITED, LIMITED_NOW_LOCKED_OUT, LOCKED_OUT;
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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.
|
||||
*
|
||||
*/
|
||||
class ServerIconLoader {
|
||||
|
||||
static int[] createServerIcon(BufferedImage awtIcon) {
|
||||
BufferedImage icon = awtIcon;
|
||||
boolean gotScaled = false;
|
||||
if(icon.getWidth() != 64 || icon.getHeight() != 64) {
|
||||
icon = new BufferedImage(64, 64, awtIcon.getType());
|
||||
Graphics2D g = (Graphics2D) icon.getGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, (awtIcon.getWidth() < 64 || awtIcon.getHeight() < 64) ?
|
||||
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR : RenderingHints.VALUE_INTERPOLATION_BICUBIC);
|
||||
g.setBackground(new Color(0, true));
|
||||
g.clearRect(0, 0, 64, 64);
|
||||
int ow = awtIcon.getWidth();
|
||||
int oh = awtIcon.getHeight();
|
||||
int nw, nh;
|
||||
float aspectRatio = (float)oh / (float)ow;
|
||||
if(aspectRatio >= 1.0f) {
|
||||
nw = (int)(64 / aspectRatio);
|
||||
nh = 64;
|
||||
}else {
|
||||
nw = 64;
|
||||
nh = (int)(64 * aspectRatio);
|
||||
}
|
||||
g.drawImage(awtIcon, (64 - nw) / 2, (64 - nh) / 2, (64 - nw) / 2 + nw, (64 - nh) / 2 + nh, 0, 0, awtIcon.getWidth(), awtIcon.getHeight(), null);
|
||||
g.dispose();
|
||||
gotScaled = true;
|
||||
}
|
||||
int[] pxls = icon.getRGB(0, 0, 64, 64, new int[4096], 0, 64);
|
||||
if(gotScaled) {
|
||||
for(int i = 0; i < pxls.length; ++i) {
|
||||
if((pxls[i] & 0xFFFFFF) == 0) {
|
||||
pxls[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return pxls;
|
||||
}
|
||||
|
||||
static int[] createServerIcon(InputStream f) {
|
||||
try {
|
||||
return createServerIcon(ImageIO.read(f));
|
||||
}catch(Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static int[] createServerIcon(File f) {
|
||||
try {
|
||||
return createServerIcon(ImageIO.read(f));
|
||||
}catch(Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.velocitypowered.api.util.GameProfile.Property;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.BinaryHttpClient;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.BinaryHttpClient.Response;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
class VanillaDefaultSkinProfileLoader implements Consumer<Response> {
|
||||
|
||||
private class ProfileSkinConsumerImpl implements Consumer<Response> {
|
||||
|
||||
private final String uuid;
|
||||
|
||||
private ProfileSkinConsumerImpl(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Response response) {
|
||||
if(response == null) {
|
||||
EaglerXVelocity.logger().error("Request error (null)");
|
||||
doNotify();
|
||||
}else if(response.exception != null) {
|
||||
EaglerXVelocity.logger().error("Exception loading vanilla default profile!", response.exception);
|
||||
doNotify();
|
||||
}else if(response.code != 200) {
|
||||
EaglerXVelocity.logger().error("Recieved code {} while looking up profile of {}", response.code, uuid);
|
||||
doNotify();
|
||||
}else if (response.data == null) {
|
||||
EaglerXVelocity.logger().error("Recieved null payload while looking up profile of {}", uuid);
|
||||
doNotify();
|
||||
}else {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(new String(response.data, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
JsonElement propsElement = json.get("properties");
|
||||
if(propsElement != null) {
|
||||
JsonArray properties = propsElement.getAsJsonArray();
|
||||
if(properties.size() > 0) {
|
||||
for(int i = 0, l = properties.size(); i < l; ++i) {
|
||||
JsonElement prop = properties.get(i);
|
||||
if(prop.isJsonObject()) {
|
||||
JsonObject propObj = prop.getAsJsonObject();
|
||||
if(propObj.get("name").getAsString().equals("textures")) {
|
||||
JsonElement value = propObj.get("value");
|
||||
JsonElement signature = propObj.get("signature");
|
||||
if(value != null) {
|
||||
Property newProp = new Property("textures", value.getAsString(),
|
||||
signature != null ? signature.getAsString() : "");
|
||||
config.eaglerPlayersVanillaSkinCached = new Property[] { newProp, EaglerVelocityConfig.isEaglerProperty };
|
||||
}
|
||||
EaglerXVelocity.logger().info("Loaded vanilla profile: " + config.getEaglerPlayersVanillaSkin());
|
||||
doNotify();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EaglerXVelocity.logger().warn("No skin was found for: {}", config.getEaglerPlayersVanillaSkin());
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception processing name to UUID lookup response!", t);
|
||||
}
|
||||
doNotify();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final EaglerVelocityConfig config;
|
||||
private volatile boolean isLocked = true;
|
||||
private final Object lock = new Object();
|
||||
|
||||
private VanillaDefaultSkinProfileLoader(EaglerVelocityConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Response response) {
|
||||
if(response == null) {
|
||||
EaglerXVelocity.logger().error("Request error (null)");
|
||||
doNotify();
|
||||
}else if(response.exception != null) {
|
||||
EaglerXVelocity.logger().error("Exception loading vanilla default profile!", response.exception);
|
||||
doNotify();
|
||||
}else if(response.code != 200) {
|
||||
EaglerXVelocity.logger().error("Recieved code {} while looking up UUID of {}", response.code, config.getEaglerPlayersVanillaSkin());
|
||||
doNotify();
|
||||
}else if (response.data == null) {
|
||||
EaglerXVelocity.logger().error("Recieved null payload while looking up UUID of {}", config.getEaglerPlayersVanillaSkin());
|
||||
doNotify();
|
||||
}else {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(new String(response.data, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
String uuid = json.get("id").getAsString();
|
||||
URI requestURI = URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false");
|
||||
BinaryHttpClient.asyncRequest("GET", requestURI, new ProfileSkinConsumerImpl(uuid));
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception processing name to UUID lookup response!", t);
|
||||
doNotify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doNotify() {
|
||||
synchronized(lock) {
|
||||
if(isLocked) {
|
||||
isLocked = false;
|
||||
lock.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void lookupVanillaSkinUser(EaglerVelocityConfig config) {
|
||||
String user = config.getEaglerPlayersVanillaSkin();
|
||||
EaglerXVelocity.logger().info("Loading vanilla profile: " + user);
|
||||
URI requestURI = URI.create("https://api.mojang.com/users/profiles/minecraft/" + user);
|
||||
VanillaDefaultSkinProfileLoader loader = new VanillaDefaultSkinProfileLoader(config);
|
||||
synchronized(loader.lock) {
|
||||
BinaryHttpClient.asyncRequest("GET", requestURI, loader);
|
||||
if(loader.isLocked) {
|
||||
try {
|
||||
loader.lock.wait(5000l);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
if(loader.isLocked) {
|
||||
EaglerXVelocity.logger().warn("Profile load timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2012, md_5. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* The name of the author may not be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* You may not use the software for commercial software hosting services without
|
||||
* written permission from the author.
|
||||
*
|
||||
* 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 OWNER 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee;
|
||||
|
||||
public class ChatColor {
|
||||
|
||||
public static final char COLOR_CHAR = '\u00A7';
|
||||
public static final String ALL_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx";
|
||||
|
||||
public static String translateAlternateColorCodes(char altColorChar, String textToTranslate) {
|
||||
char[] b = textToTranslate.toCharArray();
|
||||
for (int i = 0; i < b.length - 1; i++) {
|
||||
if (b[i] == altColorChar && ALL_CODES.indexOf(b[i + 1]) > -1) {
|
||||
b[i] = ChatColor.COLOR_CHAR;
|
||||
b[i + 1] = Character.toLowerCase(b[i + 1]);
|
||||
}
|
||||
}
|
||||
return new String(b);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,376 @@
|
||||
/*
|
||||
* Copyright (c) 2012, md_5. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* The name of the author may not be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* You may not use the software for commercial software hosting services without
|
||||
* written permission from the author.
|
||||
*
|
||||
* 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 OWNER 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class Configuration {
|
||||
|
||||
private static final char SEPARATOR = '.';
|
||||
final Map<String, Object> self;
|
||||
private final Configuration defaults;
|
||||
|
||||
public Configuration() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public Configuration(Configuration defaults) {
|
||||
this(new LinkedHashMap<String, Object>(), defaults);
|
||||
}
|
||||
|
||||
Configuration(Map<?, ?> map, Configuration defaults) {
|
||||
this.self = new LinkedHashMap<>();
|
||||
this.defaults = defaults;
|
||||
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
String key = (entry.getKey() == null) ? "null" : entry.getKey().toString();
|
||||
|
||||
if (entry.getValue() instanceof Map) {
|
||||
this.self.put(key, new Configuration((Map) entry.getValue(),
|
||||
(defaults == null) ? null : defaults.getSection(key)));
|
||||
} else {
|
||||
this.self.put(key, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Configuration getSectionFor(String path) {
|
||||
int index = path.indexOf(SEPARATOR);
|
||||
if (index == -1) {
|
||||
return this;
|
||||
}
|
||||
|
||||
String root = path.substring(0, index);
|
||||
Object section = self.get(root);
|
||||
if (section == null) {
|
||||
section = new Configuration((defaults == null) ? null : defaults.getSection(root));
|
||||
self.put(root, section);
|
||||
}
|
||||
|
||||
return (Configuration) section;
|
||||
}
|
||||
|
||||
private String getChild(String path) {
|
||||
int index = path.indexOf(SEPARATOR);
|
||||
return (index == -1) ? path : path.substring(index + 1);
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T get(String path, T def) {
|
||||
Configuration section = getSectionFor(path);
|
||||
Object val;
|
||||
if (section == this) {
|
||||
val = self.get(path);
|
||||
} else {
|
||||
val = section.get(getChild(path), def);
|
||||
}
|
||||
|
||||
if (val == null && def instanceof Configuration) {
|
||||
self.put(path, def);
|
||||
}
|
||||
|
||||
return (val != null) ? (T) val : def;
|
||||
}
|
||||
|
||||
public boolean contains(String path) {
|
||||
return get(path, null) != null;
|
||||
}
|
||||
|
||||
public Object get(String path) {
|
||||
return get(path, getDefault(path));
|
||||
}
|
||||
|
||||
public Object getDefault(String path) {
|
||||
return (defaults == null) ? null : defaults.get(path);
|
||||
}
|
||||
|
||||
public void set(String path, Object value) {
|
||||
if (value instanceof Map) {
|
||||
value = new Configuration((Map) value, (defaults == null) ? null : defaults.getSection(path));
|
||||
}
|
||||
|
||||
Configuration section = getSectionFor(path);
|
||||
if (section == this) {
|
||||
if (value == null) {
|
||||
self.remove(path);
|
||||
} else {
|
||||
self.put(path, value);
|
||||
}
|
||||
} else {
|
||||
section.set(getChild(path), value);
|
||||
}
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------*/
|
||||
public Configuration getSection(String path) {
|
||||
Object def = getDefault(path);
|
||||
return (Configuration) get(path, (def instanceof Configuration) ? def
|
||||
: new Configuration((defaults == null) ? null : defaults.getSection(path)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets keys, not deep by default.
|
||||
*
|
||||
* @return top level keys for this section
|
||||
*/
|
||||
public Collection<String> getKeys() {
|
||||
return new LinkedHashSet<>(self.keySet());
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------*/
|
||||
public byte getByte(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getByte(path, (def instanceof Number) ? ((Number) def).byteValue() : 0);
|
||||
}
|
||||
|
||||
public byte getByte(String path, byte def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof Number) ? ((Number) val).byteValue() : def;
|
||||
}
|
||||
|
||||
public List<Byte> getByteList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<Byte> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof Number) {
|
||||
result.add(((Number) object).byteValue());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public short getShort(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getShort(path, (def instanceof Number) ? ((Number) def).shortValue() : 0);
|
||||
}
|
||||
|
||||
public short getShort(String path, short def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof Number) ? ((Number) val).shortValue() : def;
|
||||
}
|
||||
|
||||
public List<Short> getShortList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<Short> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof Number) {
|
||||
result.add(((Number) object).shortValue());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getInt(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getInt(path, (def instanceof Number) ? ((Number) def).intValue() : 0);
|
||||
}
|
||||
|
||||
public int getInt(String path, int def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof Number) ? ((Number) val).intValue() : def;
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<Integer> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof Number) {
|
||||
result.add(((Number) object).intValue());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public long getLong(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getLong(path, (def instanceof Number) ? ((Number) def).longValue() : 0);
|
||||
}
|
||||
|
||||
public long getLong(String path, long def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof Number) ? ((Number) val).longValue() : def;
|
||||
}
|
||||
|
||||
public List<Long> getLongList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<Long> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof Number) {
|
||||
result.add(((Number) object).longValue());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public float getFloat(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getFloat(path, (def instanceof Number) ? ((Number) def).floatValue() : 0);
|
||||
}
|
||||
|
||||
public float getFloat(String path, float def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof Number) ? ((Number) val).floatValue() : def;
|
||||
}
|
||||
|
||||
public List<Float> getFloatList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<Float> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof Number) {
|
||||
result.add(((Number) object).floatValue());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public double getDouble(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getDouble(path, (def instanceof Number) ? ((Number) def).doubleValue() : 0);
|
||||
}
|
||||
|
||||
public double getDouble(String path, double def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof Number) ? ((Number) val).doubleValue() : def;
|
||||
}
|
||||
|
||||
public List<Double> getDoubleList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<Double> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof Number) {
|
||||
result.add(((Number) object).doubleValue());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean getBoolean(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getBoolean(path, (def instanceof Boolean) ? (Boolean) def : false);
|
||||
}
|
||||
|
||||
public boolean getBoolean(String path, boolean def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof Boolean) ? (Boolean) val : def;
|
||||
}
|
||||
|
||||
public List<Boolean> getBooleanList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<Boolean> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof Boolean) {
|
||||
result.add((Boolean) object);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public char getChar(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getChar(path, (def instanceof Character) ? (Character) def : '\u0000');
|
||||
}
|
||||
|
||||
public char getChar(String path, char def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof Character) ? (Character) val : def;
|
||||
}
|
||||
|
||||
public List<Character> getCharList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<Character> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof Character) {
|
||||
result.add((Character) object);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getString(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getString(path, (def instanceof String) ? (String) def : "");
|
||||
}
|
||||
|
||||
public String getString(String path, String def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof String) ? (String) val : def;
|
||||
}
|
||||
|
||||
public List<String> getStringList(String path) {
|
||||
List<?> list = getList(path);
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
for (Object object : list) {
|
||||
if (object instanceof String) {
|
||||
result.add((String) object);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------*/
|
||||
public List<?> getList(String path) {
|
||||
Object def = getDefault(path);
|
||||
return getList(path, (def instanceof List<?>) ? (List<?>) def : Collections.EMPTY_LIST);
|
||||
}
|
||||
|
||||
public List<?> getList(String path, List<?> def) {
|
||||
Object val = get(path, def);
|
||||
return (val instanceof List<?>) ? (List<?>) val : def;
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2012, md_5. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* The name of the author may not be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* You may not use the software for commercial software hosting services without
|
||||
* written permission from the author.
|
||||
*
|
||||
* 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 OWNER 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class ConfigurationProvider {
|
||||
|
||||
private static final Map<Class<? extends ConfigurationProvider>, ConfigurationProvider> providers = new HashMap<>();
|
||||
|
||||
static {
|
||||
try {
|
||||
providers.put(YamlConfiguration.class, new YamlConfiguration());
|
||||
} catch (NoClassDefFoundError ex) {
|
||||
// Ignore, no SnakeYAML
|
||||
}
|
||||
|
||||
try {
|
||||
providers.put(JsonConfiguration.class, new JsonConfiguration());
|
||||
} catch (NoClassDefFoundError ex) {
|
||||
// Ignore, no Gson
|
||||
}
|
||||
}
|
||||
|
||||
public static ConfigurationProvider getProvider(Class<? extends ConfigurationProvider> provider) {
|
||||
return providers.get(provider);
|
||||
}
|
||||
|
||||
/*------------------------------------------------------------------------*/
|
||||
public abstract void save(Configuration config, File file) throws IOException;
|
||||
|
||||
public abstract void save(Configuration config, Writer writer);
|
||||
|
||||
public abstract Configuration load(File file) throws IOException;
|
||||
|
||||
public abstract Configuration load(File file, Configuration defaults) throws IOException;
|
||||
|
||||
public abstract Configuration load(Reader reader);
|
||||
|
||||
public abstract Configuration load(Reader reader, Configuration defaults);
|
||||
|
||||
public abstract Configuration load(InputStream is);
|
||||
|
||||
public abstract Configuration load(InputStream is, Configuration defaults);
|
||||
|
||||
public abstract Configuration load(String string);
|
||||
|
||||
public abstract Configuration load(String string, Configuration defaults);
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2012, md_5. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* The name of the author may not be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* You may not use the software for commercial software hosting services without
|
||||
* written permission from the author.
|
||||
*
|
||||
* 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 OWNER 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class JsonConfiguration extends ConfigurationProvider {
|
||||
|
||||
private final Gson json = new GsonBuilder().serializeNulls().setPrettyPrinting()
|
||||
.registerTypeAdapter(Configuration.class, new JsonSerializer<Configuration>() {
|
||||
@Override
|
||||
public JsonElement serialize(Configuration src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return context.serialize(((Configuration) src).self);
|
||||
}
|
||||
}).create();
|
||||
|
||||
@Override
|
||||
public void save(Configuration config, File file) throws IOException {
|
||||
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
|
||||
save(config, writer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Configuration config, Writer writer) {
|
||||
json.toJson(config.self, writer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(File file) throws IOException {
|
||||
return load(file, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(File file, Configuration defaults) throws IOException {
|
||||
try (FileInputStream is = new FileInputStream(file)) {
|
||||
return load(is, defaults);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(Reader reader) {
|
||||
return load(reader, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Configuration load(Reader reader, Configuration defaults) {
|
||||
Map<String, Object> map = json.fromJson(reader, LinkedHashMap.class);
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
return new Configuration(map, defaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(InputStream is) {
|
||||
return load(is, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(InputStream is, Configuration defaults) {
|
||||
return load(new InputStreamReader(is, StandardCharsets.UTF_8), defaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(String string) {
|
||||
return load(string, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Configuration load(String string, Configuration defaults) {
|
||||
Map<String, Object> map = json.fromJson(string, LinkedHashMap.class);
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
return new Configuration(map, defaults);
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (c) 2012, md_5. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* The name of the author may not be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* You may not use the software for commercial software hosting services without
|
||||
* written permission from the author.
|
||||
*
|
||||
* 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 OWNER 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.bungee;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.Constructor;
|
||||
import org.yaml.snakeyaml.nodes.Node;
|
||||
import org.yaml.snakeyaml.representer.Represent;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
|
||||
public class YamlConfiguration extends ConfigurationProvider {
|
||||
|
||||
private final ThreadLocal<Yaml> yaml = new ThreadLocal<Yaml>() {
|
||||
@Override
|
||||
protected Yaml initialValue() {
|
||||
DumperOptions options = new DumperOptions();
|
||||
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
|
||||
Representer representer = new Representer(options) {
|
||||
{
|
||||
representers.put(Configuration.class, new Represent() {
|
||||
@Override
|
||||
public Node representData(Object data) {
|
||||
return represent(((Configuration) data).self);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return new Yaml(new Constructor(new LoaderOptions()), representer, options);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void save(Configuration config, File file) throws IOException {
|
||||
try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) {
|
||||
save(config, writer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Configuration config, Writer writer) {
|
||||
yaml.get().dump(config.self, writer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(File file) throws IOException {
|
||||
return load(file, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(File file, Configuration defaults) throws IOException {
|
||||
try (FileInputStream is = new FileInputStream(file)) {
|
||||
return load(is, defaults);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(Reader reader) {
|
||||
return load(reader, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Configuration load(Reader reader, Configuration defaults) {
|
||||
Map<String, Object> map = yaml.get().loadAs(reader, LinkedHashMap.class);
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
return new Configuration(map, defaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(InputStream is) {
|
||||
return load(is, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Configuration load(InputStream is, Configuration defaults) {
|
||||
Map<String, Object> map = yaml.get().loadAs(is, LinkedHashMap.class);
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
return new Configuration(map, defaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Configuration load(String string) {
|
||||
return load(string, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Configuration load(String string, Configuration defaults) {
|
||||
Map<String, Object> map = yaml.get().loadAs(string, LinkedHashMap.class);
|
||||
if (map == null) {
|
||||
map = new LinkedHashMap<>();
|
||||
}
|
||||
return new Configuration(map, defaults);
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.handlers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||
import com.velocitypowered.api.event.player.ServerConnectedEvent;
|
||||
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.api.util.GameProfile.Property;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth.DefaultAuthSystem;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerAuthConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPlayerData;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.Base64;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.CapePackets;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.CapeServiceOffline;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.SkinPackets;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.SkinService;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.voice.VoiceService;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.voice.VoiceSignalPackets;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 EaglerPacketEventListener {
|
||||
|
||||
public static final ChannelIdentifier FNAW_SKIN_ENABLE_CHANNEL = new LegacyChannelIdentifier("EAG|FNAWSEn-1.8");
|
||||
public static final ChannelIdentifier GET_DOMAIN_CHANNEL = new LegacyChannelIdentifier("EAG|GetDomain");
|
||||
|
||||
public final EaglerXVelocity plugin;
|
||||
|
||||
public EaglerPacketEventListener(EaglerXVelocity plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.FIRST)
|
||||
public void onPluginMessage(final PluginMessageEvent event) {
|
||||
if(event.getSource() instanceof ConnectedPlayer) {
|
||||
final ConnectedPlayer player = (ConnectedPlayer)event.getSource();
|
||||
EaglerPlayerData eagPlayer = EaglerPipeline.getEaglerHandle(player);
|
||||
if(eagPlayer != null) {
|
||||
if(SkinService.CHANNEL.equals(event.getIdentifier())) {
|
||||
EaglerXVelocity.proxy().getScheduler().buildTask(plugin, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
SkinPackets.processPacket(event.getData(), player, plugin.getSkinService());
|
||||
} catch (IOException e) {
|
||||
player.disconnect(Component.text("Skin packet error!"));
|
||||
EaglerXVelocity.logger().error("Eagler user \"{}\" raised an exception handling skins!", player.getUsername(), e);
|
||||
}
|
||||
}
|
||||
}).schedule();
|
||||
}else if(CapeServiceOffline.CHANNEL.equals(event.getIdentifier())) {
|
||||
try {
|
||||
CapePackets.processPacket(event.getData(), player, plugin.getCapeService());
|
||||
} catch (IOException e) {
|
||||
player.disconnect(Component.text("Cape packet error!"));
|
||||
EaglerXVelocity.logger().error("Eagler user \"{}\" raised an exception handling capes!", player.getUsername(), e);
|
||||
}
|
||||
}else if(VoiceService.CHANNEL.equals(event.getIdentifier())) {
|
||||
VoiceService svc = plugin.getVoiceService();
|
||||
if(svc != null && eagPlayer.getEaglerListenerConfig().getEnableVoiceChat()) {
|
||||
try {
|
||||
VoiceSignalPackets.processPacket(event.getData(), player, svc);
|
||||
} catch (IOException e) {
|
||||
player.disconnect(Component.text("Voice signal packet error!"));
|
||||
EaglerXVelocity.logger().error("Eagler user \"{}\" raised an exception handling voice signals!", player.getUsername(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}else if(event.getSource() instanceof ServerConnection && event.getTarget() instanceof ConnectedPlayer) {
|
||||
ConnectedPlayer player = (ConnectedPlayer)event.getTarget();
|
||||
if(GET_DOMAIN_CHANNEL.equals(event.getIdentifier())) {
|
||||
EaglerPlayerData eagPlayerData = EaglerPipeline.getEaglerHandle(player);
|
||||
if(eagPlayerData != null) {
|
||||
String domain = eagPlayerData.getOrigin();
|
||||
if(domain == null) {
|
||||
((ServerConnection)event.getSource()).sendPluginMessage(GET_DOMAIN_CHANNEL, new byte[] { 0 });
|
||||
}else {
|
||||
((ServerConnection)event.getSource()).sendPluginMessage(GET_DOMAIN_CHANNEL, domain.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
Player p = event.getPlayer();
|
||||
if(p instanceof ConnectedPlayer) {
|
||||
ConnectedPlayer player = (ConnectedPlayer)p;
|
||||
GameProfile res = player.getGameProfile();
|
||||
if(res != null) {
|
||||
List<Property> props = res.getProperties();
|
||||
if(props.size() > 0) {
|
||||
for(int i = 0, l = props.size(); i < l; ++i) {
|
||||
Property pp = props.get(i);
|
||||
if(pp.getName().equals("textures")) {
|
||||
try {
|
||||
String jsonStr = SkinPackets.bytesToAscii(Base64.decodeBase64(pp.getValue()));
|
||||
JsonObject json = JsonParser.parseString(jsonStr).getAsJsonObject();
|
||||
JsonObject skinObj = json.getAsJsonObject("SKIN");
|
||||
if(skinObj != null) {
|
||||
JsonElement url = json.get("url");
|
||||
if(url != null) {
|
||||
String urlStr = SkinService.sanitizeTextureURL(url.getAsString());
|
||||
plugin.getSkinService().registerTextureToPlayerAssociation(urlStr, player.getUniqueId());
|
||||
}
|
||||
}
|
||||
}catch(Throwable t) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EaglerAuthConfig authConf = plugin.getConfig().getAuthConfig();
|
||||
if(authConf.isEnableAuthentication() && authConf.isUseBuiltInAuthentication()) {
|
||||
DefaultAuthSystem srv = plugin.getAuthService();
|
||||
if(srv != null) {
|
||||
srv.handleVanillaLogin(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onConnectionLost(DisconnectEvent event) {
|
||||
UUID uuid = event.getPlayer().getUniqueId();
|
||||
plugin.getSkinService().unregisterPlayer(uuid);
|
||||
plugin.getCapeService().unregisterPlayer(uuid);
|
||||
if(event.getPlayer() instanceof ConnectedPlayer) {
|
||||
ConnectedPlayer player = (ConnectedPlayer)event.getPlayer();
|
||||
EaglerPlayerData eagData = EaglerPipeline.getEaglerHandle(player);
|
||||
if(eagData != null && eagData.getEaglerListenerConfig().getEnableVoiceChat()) {
|
||||
plugin.getVoiceService().handlePlayerLoggedOut(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onServerConnected(ServerConnectedEvent event) {
|
||||
if(event.getPlayer() instanceof ConnectedPlayer) {
|
||||
ConnectedPlayer player = (ConnectedPlayer)event.getPlayer();
|
||||
EaglerPlayerData eagData = EaglerPipeline.getEaglerHandle(player);
|
||||
if(eagData != null) {
|
||||
ServerInfo sv = event.getServer().getServerInfo();
|
||||
boolean fnawSkins = !plugin.getConfig().getDisableFNAWSkinsEverywhere()
|
||||
&& !plugin.getConfig().getDisableFNAWSkinsOnServersSet().contains(sv.getName());
|
||||
if(fnawSkins != eagData.currentFNAWSkinEnableStatus) {
|
||||
eagData.currentFNAWSkinEnableStatus = fnawSkins;
|
||||
player.sendPluginMessage(FNAW_SKIN_ENABLE_CHANNEL, new byte[] { fnawSkins ? (byte)1 : (byte)0 });
|
||||
}
|
||||
if(eagData.getEaglerListenerConfig().getEnableVoiceChat()) {
|
||||
plugin.getVoiceService().handleServerConnected(player, sv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onServerDisconnected(ServerPreConnectEvent event) {
|
||||
if(event.getPreviousServer() != null && (event.getPlayer() instanceof ConnectedPlayer)) {
|
||||
ConnectedPlayer player = (ConnectedPlayer)event.getPlayer();
|
||||
EaglerPlayerData eagData = EaglerPipeline.getEaglerHandle(player);
|
||||
if(eagData != null && eagData.getEaglerListenerConfig().getEnableVoiceChat()) {
|
||||
plugin.getVoiceService().handleServerDisconnected(player, event.getPreviousServer().getServerInfo());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
|
||||
/**
|
||||
* 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 EaglerConnectionInstance {
|
||||
|
||||
public final Channel channel;
|
||||
|
||||
public final long creationTime;
|
||||
public boolean hasBeenForwarded = false;
|
||||
|
||||
public long lastServerPingPacket;
|
||||
public long lastClientPingPacket;
|
||||
public long lastClientPongPacket;
|
||||
|
||||
public boolean isWebSocket = false;
|
||||
public boolean isRegularHttp = false;
|
||||
|
||||
public ConnectedPlayer userConnection = null;
|
||||
public EaglerPlayerData eaglerData = null;
|
||||
|
||||
public EaglerConnectionInstance(Channel channel) {
|
||||
this.channel = channel;
|
||||
this.creationTime = this.lastServerPingPacket = this.lastClientPingPacket =
|
||||
this.lastClientPongPacket = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 EaglerMinecraftDecoder extends MessageToMessageDecoder<WebSocketFrame> {
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List<Object> out) {
|
||||
if(!ctx.channel().isActive()) {
|
||||
return;
|
||||
}
|
||||
EaglerConnectionInstance con = ctx.channel().attr(EaglerPipeline.CONNECTION_INSTANCE).get();
|
||||
long millis = System.currentTimeMillis();
|
||||
if(frame instanceof BinaryWebSocketFrame) {
|
||||
out.add(frame.content().retain());
|
||||
}else if(frame instanceof PingWebSocketFrame) {
|
||||
if(millis - con.lastClientPingPacket > 500l) {
|
||||
ctx.write(new PongWebSocketFrame());
|
||||
con.lastClientPingPacket = millis;
|
||||
}
|
||||
}else if(frame instanceof PongWebSocketFrame) {
|
||||
con.lastClientPongPacket = millis;
|
||||
}else {
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageEncoder;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 EaglerMinecraftEncoder extends MessageToMessageEncoder<ByteBuf> {
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) {
|
||||
out.add(new BinaryWebSocketFrame(msg.retain()));
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.WriteBufferWaterMark;
|
||||
import io.netty.handler.codec.compression.ZlibCodecFactory;
|
||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||
import io.netty.handler.codec.http.HttpServerCodec;
|
||||
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandler;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.DeflateFrameServerExtensionHandshaker;
|
||||
import io.netty.handler.codec.http.websocketx.extensions.compression.PerMessageDeflateServerExtensionHandshaker;
|
||||
import io.netty.util.AttributeKey;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerVelocityConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPlayerData.ClientCertificateHolder;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web.HttpWebServer;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 EaglerPipeline {
|
||||
|
||||
public static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1048576, 2097152);
|
||||
public static final AttributeKey<EaglerListenerConfig> LISTENER = AttributeKey.valueOf("ListenerInfo");
|
||||
public static final AttributeKey<InetSocketAddress> LOCAL_ADDRESS = AttributeKey.valueOf("LocalAddress");
|
||||
public static final AttributeKey<EaglerConnectionInstance> CONNECTION_INSTANCE = AttributeKey.valueOf("EaglerConnectionInstance");
|
||||
public static final AttributeKey<InetAddress> REAL_ADDRESS = AttributeKey.valueOf("RealAddress");
|
||||
public static final AttributeKey<String> HOST = AttributeKey.valueOf("Host");
|
||||
public static final AttributeKey<String> ORIGIN = AttributeKey.valueOf("Origin");
|
||||
|
||||
public static final Collection<Channel> openChannels = new LinkedList();
|
||||
|
||||
public static final ChannelIdentifier UPDATE_CERT_CHANNEL = new LegacyChannelIdentifier("EAG|UpdateCert-1.8");
|
||||
|
||||
public static final TimerTask closeInactive = new TimerTask() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Logger log = EaglerXVelocity.logger();
|
||||
try {
|
||||
EaglerVelocityConfig conf = EaglerXVelocity.getEagler().getConfig();
|
||||
long handshakeTimeout = conf.getWebsocketHandshakeTimeout();
|
||||
long keepAliveTimeout = conf.getWebsocketKeepAliveTimeout();
|
||||
List<Channel> channelsList;
|
||||
synchronized(openChannels) {
|
||||
long millis = System.currentTimeMillis();
|
||||
Iterator<Channel> channelIterator = openChannels.iterator();
|
||||
while(channelIterator.hasNext()) {
|
||||
Channel c = channelIterator.next();
|
||||
final EaglerConnectionInstance i = c.attr(EaglerPipeline.CONNECTION_INSTANCE).get();
|
||||
long handshakeTimeoutForConnection = 500l;
|
||||
if(i.isRegularHttp) handshakeTimeoutForConnection = 10000l;
|
||||
if(i.isWebSocket) handshakeTimeoutForConnection = handshakeTimeout;
|
||||
if(i == null || (!i.hasBeenForwarded && millis - i.creationTime > handshakeTimeoutForConnection)
|
||||
|| millis - i.lastClientPongPacket > keepAliveTimeout || !c.isActive()) {
|
||||
if(c.isActive()) {
|
||||
c.close();
|
||||
}
|
||||
channelIterator.remove();
|
||||
}else {
|
||||
long pingRate = 5000l;
|
||||
if(pingRate + 700l > keepAliveTimeout) {
|
||||
pingRate = keepAliveTimeout - 500l;
|
||||
if(pingRate < 500l) {
|
||||
keepAliveTimeout = 500l;
|
||||
}
|
||||
}
|
||||
if(millis - i.lastServerPingPacket > pingRate) {
|
||||
i.lastServerPingPacket = millis;
|
||||
c.write(new PingWebSocketFrame());
|
||||
}
|
||||
}
|
||||
}
|
||||
channelsList = new ArrayList(openChannels);
|
||||
}
|
||||
for(EaglerListenerConfig lst : conf.getServerListeners()) {
|
||||
HttpWebServer srv = lst.getWebServer();
|
||||
if(srv != null) {
|
||||
try {
|
||||
srv.flushCache();
|
||||
}catch(Throwable t) {
|
||||
log.error("Failed to flush web server cache for: {}", lst.getAddress().toString());
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!conf.getUpdateConfig().isBlockAllClientUpdates()) {
|
||||
int sizeTracker = 0;
|
||||
for(Channel c : channelsList) {
|
||||
EaglerConnectionInstance conn = c.attr(EaglerPipeline.CONNECTION_INSTANCE).get();
|
||||
if(conn.userConnection == null) {
|
||||
continue;
|
||||
}
|
||||
EaglerPlayerData i = conn.eaglerData;
|
||||
ClientCertificateHolder certHolder = null;
|
||||
synchronized(i.certificatesToSend) {
|
||||
if(i.certificatesToSend.size() > 0) {
|
||||
Iterator<ClientCertificateHolder> itr = i.certificatesToSend.iterator();
|
||||
certHolder = itr.next();
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
if(certHolder != null) {
|
||||
int identityHash = certHolder.hashCode();
|
||||
boolean bb;
|
||||
synchronized(i.certificatesSent) {
|
||||
bb = i.certificatesSent.add(identityHash);
|
||||
}
|
||||
if(bb) {
|
||||
conn.userConnection.sendPluginMessage(UPDATE_CERT_CHANNEL, certHolder.data);
|
||||
sizeTracker += certHolder.data.length;
|
||||
if(sizeTracker > (conf.getUpdateConfig().getCertPacketDataRateLimit() / 4)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EaglerUpdateSvc.updateTick();
|
||||
}
|
||||
}catch(Throwable t) {
|
||||
log.error("Exception in thread \"{}\"! {}", Thread.currentThread().getName(), t.toString());
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static final ChannelInitializer<Channel> SERVER_CHILD = new ChannelInitializer<Channel>() {
|
||||
|
||||
@Override
|
||||
protected void initChannel(Channel channel) throws Exception {
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
pipeline.addLast("HttpServerCodec", new HttpServerCodec());
|
||||
pipeline.addLast("HttpObjectAggregator", new HttpObjectAggregator(65535));
|
||||
int compressionLevel = EaglerXVelocity.getEagler().getConfig().getHttpWebsocketCompressionLevel();
|
||||
if(compressionLevel > 0) {
|
||||
if(compressionLevel > 9) {
|
||||
compressionLevel = 9;
|
||||
}
|
||||
DeflateFrameServerExtensionHandshaker deflateExtensionHandshaker = new DeflateFrameServerExtensionHandshaker(
|
||||
compressionLevel);
|
||||
PerMessageDeflateServerExtensionHandshaker perMessageDeflateExtensionHandshaker = new PerMessageDeflateServerExtensionHandshaker(
|
||||
compressionLevel, ZlibCodecFactory.isSupportingWindowSizeAndMemLevel(),
|
||||
PerMessageDeflateServerExtensionHandshaker.MAX_WINDOW_SIZE, false, false);
|
||||
pipeline.addLast("HttpCompressionHandler", new WebSocketServerExtensionHandler(deflateExtensionHandshaker,
|
||||
perMessageDeflateExtensionHandshaker));
|
||||
}
|
||||
pipeline.addLast("HttpHandshakeHandler", new HttpHandshakeHandler(channel.attr(LISTENER).get()));
|
||||
channel.attr(CONNECTION_INSTANCE).set(new EaglerConnectionInstance(channel));
|
||||
synchronized(openChannels) {
|
||||
openChannels.add(channel);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public static void closeChannel(Channel channel) {
|
||||
synchronized(openChannels) {
|
||||
openChannels.remove(channel);
|
||||
}
|
||||
}
|
||||
|
||||
public static EaglerPlayerData getEaglerHandle(Channel channel) {
|
||||
EaglerConnectionInstance i = channel.attr(EaglerPipeline.CONNECTION_INSTANCE).get();
|
||||
return i == null ? null : i.eaglerData;
|
||||
}
|
||||
|
||||
public static EaglerPlayerData getEaglerHandle(Player player) {
|
||||
if(!(player instanceof ConnectedPlayer)) return null;
|
||||
return getEaglerHandle(((ConnectedPlayer)player).getConnection().getChannel());
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.SimpleRateLimiter;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 EaglerPlayerData {
|
||||
|
||||
public static class ClientCertificateHolder {
|
||||
public final byte[] data;
|
||||
public final int hash;
|
||||
public ClientCertificateHolder(byte[] data, int hash) {
|
||||
this.data = data;
|
||||
this.hash = hash;
|
||||
}
|
||||
}
|
||||
|
||||
public final SimpleRateLimiter skinLookupRateLimiter;
|
||||
public final SimpleRateLimiter skinUUIDLookupRateLimiter;
|
||||
public final SimpleRateLimiter skinTextureDownloadRateLimiter;
|
||||
public final SimpleRateLimiter capeLookupRateLimiter;
|
||||
public final SimpleRateLimiter voiceConnectRateLimiter;
|
||||
public final EaglerListenerConfig listener;
|
||||
public final String origin;
|
||||
public final ClientCertificateHolder clientCertificate;
|
||||
public final Set<ClientCertificateHolder> certificatesToSend;
|
||||
public final Set<Integer> certificatesSent;
|
||||
public boolean currentFNAWSkinEnableStatus = true;
|
||||
|
||||
public EaglerPlayerData(EaglerListenerConfig listener, String origin, ClientCertificateHolder clientCertificate) {
|
||||
this.listener = listener;
|
||||
this.origin = origin;
|
||||
this.skinLookupRateLimiter = new SimpleRateLimiter();
|
||||
this.skinUUIDLookupRateLimiter = new SimpleRateLimiter();
|
||||
this.skinTextureDownloadRateLimiter = new SimpleRateLimiter();
|
||||
this.capeLookupRateLimiter = new SimpleRateLimiter();
|
||||
this.voiceConnectRateLimiter = new SimpleRateLimiter();
|
||||
this.clientCertificate = clientCertificate;
|
||||
this.certificatesToSend = new HashSet();
|
||||
this.certificatesSent = new HashSet();
|
||||
if(clientCertificate != null) {
|
||||
this.certificatesSent.add(clientCertificate.hashCode());
|
||||
}
|
||||
}
|
||||
|
||||
public String getOrigin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
public EaglerListenerConfig getEaglerListenerConfig() {
|
||||
return listener;
|
||||
}
|
||||
}
|
@ -0,0 +1,299 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocityVersion;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.auth.SHA1Digest;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerUpdateConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPlayerData.ClientCertificateHolder;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 EaglerUpdateSvc {
|
||||
|
||||
private static final List<ClientCertificateHolder> certs = new ArrayList();
|
||||
private static final Map<String,CachedClientCertificate> certsCache = new HashMap();
|
||||
|
||||
private static class CachedClientCertificate {
|
||||
private final ClientCertificateHolder cert;
|
||||
private final long lastModified;
|
||||
public CachedClientCertificate(ClientCertificateHolder cert, long lastModified) {
|
||||
this.cert = cert;
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
}
|
||||
|
||||
private static long lastDownload = 0l;
|
||||
private static long lastEnumerate = 0l;
|
||||
|
||||
public static void updateTick() {
|
||||
Logger log = EaglerXVelocity.logger();
|
||||
long millis = System.currentTimeMillis();
|
||||
EaglerUpdateConfig conf = EaglerXVelocity.getEagler().getConfig().getUpdateConfig();
|
||||
if(conf.isDownloadLatestCerts() && millis - lastDownload > (long)conf.getCheckForUpdatesEvery() * 1000l) {
|
||||
lastDownload = millis;
|
||||
lastEnumerate = 0l;
|
||||
try {
|
||||
downloadUpdates();
|
||||
}catch(Throwable t) {
|
||||
log.error("Uncaught exception downloading certificates!");
|
||||
t.printStackTrace();
|
||||
}
|
||||
millis = System.currentTimeMillis();
|
||||
}
|
||||
if(conf.isEnableEagcertFolder() && millis - lastEnumerate > 5000l) {
|
||||
lastEnumerate = millis;
|
||||
try {
|
||||
enumerateEagcertDirectory();
|
||||
}catch(Throwable t) {
|
||||
log.error("Uncaught exception reading eagcert directory!");
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadUpdates() throws Throwable {
|
||||
Logger log = EaglerXVelocity.logger();
|
||||
EaglerUpdateConfig conf = EaglerXVelocity.getEagler().getConfig().getUpdateConfig();
|
||||
File eagcert = new File(EaglerXVelocity.getEagler().getDataFolder(), "eagcert");
|
||||
if(!eagcert.isDirectory()) {
|
||||
if(!eagcert.mkdirs()) {
|
||||
log.error("Could not create directory: {}", eagcert.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
}
|
||||
Set<String> filenames = new HashSet();
|
||||
for(String str : conf.getDownloadCertURLs()) {
|
||||
try {
|
||||
URL url = new URL(str);
|
||||
HttpURLConnection con = (HttpURLConnection)url.openConnection();
|
||||
con.setDoInput(true);
|
||||
con.setDoOutput(false);
|
||||
con.setRequestMethod("GET");
|
||||
con.setConnectTimeout(30000);
|
||||
con.setReadTimeout(30000);
|
||||
con.setRequestProperty("User-Agent", "Mozilla/5.0 " + EaglerXVelocityVersion.ID + "/" + EaglerXVelocityVersion.VERSION);
|
||||
con.connect();
|
||||
int code = con.getResponseCode();
|
||||
if(code / 100 != 2) {
|
||||
con.disconnect();
|
||||
throw new IOException("Response code was " + code);
|
||||
}
|
||||
ByteArrayOutputStream bao = new ByteArrayOutputStream(32767);
|
||||
try(InputStream is = con.getInputStream()) {
|
||||
byte[] readBuffer = new byte[1024];
|
||||
int c;
|
||||
while((c = is.read(readBuffer, 0, 1024)) != -1) {
|
||||
bao.write(readBuffer, 0, c);
|
||||
}
|
||||
}
|
||||
byte[] done = bao.toByteArray();
|
||||
SHA1Digest digest = new SHA1Digest();
|
||||
digest.update(done, 0, done.length);
|
||||
byte[] hash = new byte[20];
|
||||
digest.doFinal(hash, 0);
|
||||
char[] hexchars = new char[40];
|
||||
for(int i = 0; i < 20; ++i) {
|
||||
hexchars[i << 1] = hex[(hash[i] >> 4) & 15];
|
||||
hexchars[(i << 1) + 1] = hex[hash[i] & 15];
|
||||
}
|
||||
String strr = "$dl." + new String(hexchars) + ".cert";
|
||||
filenames.add(strr);
|
||||
File cacheFile = new File(eagcert, strr);
|
||||
if(cacheFile.exists()) {
|
||||
continue; // no change
|
||||
}
|
||||
try(OutputStream os = new FileOutputStream(cacheFile)) {
|
||||
os.write(done);
|
||||
}
|
||||
log.info("Downloading new certificate: {}", str);
|
||||
}catch(Throwable t) {
|
||||
log.error("Failed to download certificate: {}", str);
|
||||
log.error("Reason: {}", t.toString());
|
||||
}
|
||||
}
|
||||
long millis = System.currentTimeMillis();
|
||||
File[] dirList = eagcert.listFiles();
|
||||
for(int i = 0; i < dirList.length; ++i) {
|
||||
File f = dirList[i];
|
||||
String n = f.getName();
|
||||
if(!n.startsWith("$dl.")) {
|
||||
continue;
|
||||
}
|
||||
if(millis - f.lastModified() > 86400000l && !filenames.contains(n)) {
|
||||
log.warn("Deleting stale certificate: {}", n);
|
||||
if(!f.delete()) {
|
||||
log.error("Failed to delete: {}", n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final char[] hex = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
|
||||
|
||||
private static void enumerateEagcertDirectory() throws Throwable {
|
||||
Logger log = EaglerXVelocity.logger();
|
||||
File eagcert = new File(EaglerXVelocity.getEagler().getDataFolder(), "eagcert");
|
||||
if(!eagcert.isDirectory()) {
|
||||
if(!eagcert.mkdirs()) {
|
||||
log.error("Could not create directory: {}", eagcert.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
}
|
||||
boolean dirty = false;
|
||||
File[] dirList = eagcert.listFiles();
|
||||
Set<String> existingFiles = new HashSet();
|
||||
for(int i = 0; i < dirList.length; ++i) {
|
||||
File f = dirList[i];
|
||||
String n = f.getName();
|
||||
long lastModified = f.lastModified();
|
||||
existingFiles.add(n);
|
||||
CachedClientCertificate cc = certsCache.get(n);
|
||||
if(cc != null) {
|
||||
if(cc.lastModified != lastModified) {
|
||||
try {
|
||||
byte[] fileData = new byte[(int)f.length()];
|
||||
if(fileData.length > 0xFFFF) {
|
||||
throw new IOException("File is too long! Max: 65535 bytes");
|
||||
}
|
||||
try(FileInputStream fis = new FileInputStream(f)) {
|
||||
for(int j = 0, k; (k = fis.read(fileData, j, fileData.length - j)) != -1 && j < fileData.length; j += k);
|
||||
}
|
||||
certsCache.remove(n);
|
||||
ClientCertificateHolder ch = tryMakeHolder(fileData);
|
||||
certsCache.put(n, new CachedClientCertificate(ch, lastModified));
|
||||
dirty = true;
|
||||
sendCertificateToPlayers(ch);
|
||||
log.info("Reloaded certificate: {}", f.getAbsolutePath());
|
||||
}catch(IOException ex) {
|
||||
log.error("Failed to read: {}", f.getAbsolutePath());
|
||||
log.error("Reason: {}", ex.toString());
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
byte[] fileData = new byte[(int)f.length()];
|
||||
if(fileData.length > 0xFFFF) {
|
||||
throw new IOException("File is too long! Max: 65535 bytes");
|
||||
}
|
||||
try(FileInputStream fis = new FileInputStream(f)) {
|
||||
for(int j = 0, k; j < fileData.length && (k = fis.read(fileData, j, fileData.length - j)) != -1; j += k);
|
||||
}
|
||||
ClientCertificateHolder ch = tryMakeHolder(fileData);
|
||||
certsCache.put(n, new CachedClientCertificate(ch, lastModified));
|
||||
dirty = true;
|
||||
sendCertificateToPlayers(ch);
|
||||
log.info("Loaded certificate: {}", f.getAbsolutePath());
|
||||
}catch(IOException ex) {
|
||||
log.error("Failed to read: {}", f.getAbsolutePath());
|
||||
log.error("Reason: {}", ex.toString());
|
||||
}
|
||||
}
|
||||
Iterator<String> itr = certsCache.keySet().iterator();
|
||||
while(itr.hasNext()) {
|
||||
String etr = itr.next();
|
||||
if(!existingFiles.contains(etr)) {
|
||||
itr.remove();
|
||||
dirty = true;
|
||||
log.warn("Certificate was deleted: {}", etr);
|
||||
}
|
||||
}
|
||||
if(dirty) {
|
||||
remakeCertsList();
|
||||
}
|
||||
}
|
||||
|
||||
private static void remakeCertsList() {
|
||||
synchronized(certs) {
|
||||
certs.clear();
|
||||
for(CachedClientCertificate cc : certsCache.values()) {
|
||||
certs.add(cc.cert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ClientCertificateHolder> getCertList() {
|
||||
return certs;
|
||||
}
|
||||
|
||||
public static ClientCertificateHolder tryMakeHolder(byte[] data) {
|
||||
int hash = Arrays.hashCode(data);
|
||||
ClientCertificateHolder ret = tryGetHolder(data, hash);
|
||||
if(ret == null) {
|
||||
ret = new ClientCertificateHolder(data, hash);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static ClientCertificateHolder tryGetHolder(byte[] data, int hash) {
|
||||
synchronized(certs) {
|
||||
for(ClientCertificateHolder cc : certs) {
|
||||
if(cc.hash == hash && Arrays.equals(cc.data, data)) {
|
||||
return cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
for(Player p : EaglerXVelocity.proxy().getAllPlayers()) {
|
||||
EaglerPlayerData pp = EaglerPipeline.getEaglerHandle(p);
|
||||
if(pp != null) {
|
||||
if(pp.clientCertificate != null && pp.clientCertificate.hash == hash && Arrays.equals(pp.clientCertificate.data, data)) {
|
||||
return pp.clientCertificate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void sendCertificateToPlayers(ClientCertificateHolder holder) {
|
||||
for(Player p : EaglerXVelocity.proxy().getAllPlayers()) {
|
||||
EaglerPlayerData pp = EaglerPipeline.getEaglerHandle(p);
|
||||
if(pp != null) {
|
||||
boolean bb;
|
||||
synchronized(pp.certificatesSent) {
|
||||
bb = pp.certificatesSent.contains(holder.hashCode());
|
||||
}
|
||||
if(!bb) {
|
||||
Set<ClientCertificateHolder> set = pp.certificatesToSend;
|
||||
synchronized(set) {
|
||||
set.add(holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 HandshakePacketTypes {
|
||||
|
||||
public static final String AUTHENTICATION_REQUIRED = "Authentication Required:";
|
||||
|
||||
public static final int PROTOCOL_CLIENT_VERSION = 0x01;
|
||||
public static final int PROTOCOL_SERVER_VERSION = 0x02;
|
||||
public static final int PROTOCOL_VERSION_MISMATCH = 0x03;
|
||||
public static final int PROTOCOL_CLIENT_REQUEST_LOGIN = 0x04;
|
||||
public static final int PROTOCOL_SERVER_ALLOW_LOGIN = 0x05;
|
||||
public static final int PROTOCOL_SERVER_DENY_LOGIN = 0x06;
|
||||
public static final int PROTOCOL_CLIENT_PROFILE_DATA = 0x07;
|
||||
public static final int PROTOCOL_CLIENT_FINISH_LOGIN = 0x08;
|
||||
public static final int PROTOCOL_SERVER_FINISH_LOGIN = 0x09;
|
||||
public static final int PROTOCOL_SERVER_ERROR = 0xFF;
|
||||
|
||||
public static final int STATE_OPENED = 0x00;
|
||||
public static final int STATE_CLIENT_VERSION = 0x01;
|
||||
public static final int STATE_CLIENT_LOGIN = 0x02;
|
||||
public static final int STATE_CLIENT_COMPLETE = 0x03;
|
||||
public static final int STATE_STALLING = 0xFF;
|
||||
|
||||
public static final int SERVER_ERROR_UNKNOWN_PACKET = 0x01;
|
||||
public static final int SERVER_ERROR_INVALID_PACKET = 0x02;
|
||||
public static final int SERVER_ERROR_WRONG_PACKET = 0x03;
|
||||
public static final int SERVER_ERROR_EXCESSIVE_PROFILE_DATA = 0x04;
|
||||
public static final int SERVER_ERROR_DUPLICATE_PROFILE_DATA = 0x05;
|
||||
public static final int SERVER_ERROR_RATELIMIT_BLOCKED = 0x06;
|
||||
public static final int SERVER_ERROR_RATELIMIT_LOCKED = 0x07;
|
||||
public static final int SERVER_ERROR_CUSTOM_MESSAGE = 0x08;
|
||||
public static final int SERVER_ERROR_AUTHENTICATION_REQUIRED = 0x09;
|
||||
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerRateLimiter;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.RateLimitStatus;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web.HttpMemoryCache;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web.HttpWebServer;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 HttpHandshakeHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static final byte[] error429Bytes = "<h3>429 Too Many Requests<br /><small>(Try again later)</small></h3>".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private final EaglerListenerConfig conf;
|
||||
|
||||
public HttpHandshakeHandler(EaglerListenerConfig conf) {
|
||||
this.conf = conf;
|
||||
}
|
||||
|
||||
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
try {
|
||||
if (msg instanceof HttpRequest) {
|
||||
EaglerConnectionInstance pingTracker = ctx.channel().attr(EaglerPipeline.CONNECTION_INSTANCE).get();
|
||||
HttpRequest req = (HttpRequest) msg;
|
||||
HttpHeaders headers = req.headers();
|
||||
|
||||
String rateLimitHost = null;
|
||||
|
||||
if (conf.isForwardIp()) {
|
||||
String str = headers.get(conf.getForwardIpHeader());
|
||||
if (str != null) {
|
||||
rateLimitHost = str.split(",", 2)[0];
|
||||
try {
|
||||
ctx.channel().attr(EaglerPipeline.REAL_ADDRESS).set(InetAddress.getByName(rateLimitHost));
|
||||
} catch (UnknownHostException ex) {
|
||||
EaglerXVelocity.logger().warn("[" + ctx.channel().remoteAddress() + "]: Connected with an invalid '" + conf.getForwardIpHeader() + "' header, disconnecting...");
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
EaglerXVelocity.logger().warn("[" + ctx.channel().remoteAddress() + "]: Connected without a '" + conf.getForwardIpHeader() + "' header, disconnecting...");
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
SocketAddress addr = ctx.channel().remoteAddress();
|
||||
if (addr instanceof InetSocketAddress) {
|
||||
rateLimitHost = ((InetSocketAddress) addr).getAddress().getHostAddress();
|
||||
}
|
||||
}
|
||||
|
||||
EaglerRateLimiter ipRateLimiter = conf.getRatelimitIp();
|
||||
RateLimitStatus ipRateLimit = RateLimitStatus.OK;
|
||||
|
||||
if (ipRateLimiter != null && rateLimitHost != null) {
|
||||
ipRateLimit = ipRateLimiter.rateLimit(rateLimitHost);
|
||||
}
|
||||
|
||||
if (ipRateLimit == RateLimitStatus.LOCKED_OUT) {
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (headers.get(HttpHeaderNames.CONNECTION) != null && headers.get(HttpHeaderNames.CONNECTION).toLowerCase().contains("upgrade") &&
|
||||
"websocket".equalsIgnoreCase(headers.get(HttpHeaderNames.UPGRADE))) {
|
||||
|
||||
String origin = headers.get(HttpHeaderNames.ORIGIN);
|
||||
if (origin != null) {
|
||||
ctx.channel().attr(EaglerPipeline.ORIGIN).set(origin);
|
||||
}
|
||||
|
||||
//TODO: origin blacklist
|
||||
|
||||
if (ipRateLimit == RateLimitStatus.OK) {
|
||||
ctx.channel().attr(EaglerPipeline.HOST).set(headers.get(HttpHeaderNames.HOST));
|
||||
ctx.pipeline().replace(this, "HttpWebSocketHandler", new HttpWebSocketHandler(conf));
|
||||
}
|
||||
|
||||
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
|
||||
"ws://" + headers.get(HttpHeaderNames.HOST) + req.uri(), null, true, 0xFFFFF);
|
||||
WebSocketServerHandshaker hs = wsFactory.newHandshaker(req);
|
||||
if (hs != null) {
|
||||
pingTracker.isWebSocket = true;
|
||||
ChannelFuture future = hs.handshake(ctx.channel(), req);
|
||||
if (ipRateLimit != RateLimitStatus.OK) {
|
||||
final RateLimitStatus rateLimitTypeFinal = ipRateLimit;
|
||||
future.addListener(new ChannelFutureListener() {
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture paramF) throws Exception {
|
||||
ctx.writeAndFlush(new TextWebSocketFrame(
|
||||
rateLimitTypeFinal == RateLimitStatus.LIMITED_NOW_LOCKED_OUT ? "LOCKED" : "BLOCKED"))
|
||||
.addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
} else {
|
||||
if (ipRateLimit != RateLimitStatus.OK) {
|
||||
ByteBuf error429Buffer = ctx.alloc().buffer(error429Bytes.length, error429Bytes.length);
|
||||
error429Buffer.writeBytes(error429Bytes);
|
||||
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.TOO_MANY_REQUESTS, error429Buffer);
|
||||
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
|
||||
return;
|
||||
}
|
||||
pingTracker.isRegularHttp = true;
|
||||
HttpWebServer srv = conf.getWebServer();
|
||||
if (srv != null) {
|
||||
String uri = req.uri();
|
||||
if (uri.startsWith("/")) {
|
||||
uri = uri.substring(1);
|
||||
}
|
||||
int j = uri.indexOf('?');
|
||||
if (j != -1) {
|
||||
uri = uri.substring(0, j);
|
||||
}
|
||||
HttpMemoryCache ch = srv.retrieveFile(uri);
|
||||
if (ch != null) {
|
||||
ctx.writeAndFlush(ch.createHTTPResponse()).addListener(ChannelFutureListener.CLOSE);
|
||||
} else {
|
||||
ctx.writeAndFlush(HttpWebServer.getWebSocket404().createHTTPResponse(HttpResponseStatus.NOT_FOUND))
|
||||
.addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
} else {
|
||||
ctx.writeAndFlush(HttpWebServer.getWebSocket404().createHTTPResponse(HttpResponseStatus.NOT_FOUND))
|
||||
.addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ctx.close();
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
if (ctx.channel().isActive()) {
|
||||
EaglerXVelocity.logger().warn("[Pre][" + ctx.channel().remoteAddress() + "]: Exception Caught: " + cause.toString(), cause);
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatAddressFor404(String str) {
|
||||
return "<span style=\"font-family:monospace;font-weight:bold;background-color:#EEEEEE;padding:3px 4px;\">" + str.replace("<", "<").replace(">", ">") + "</span>";
|
||||
}
|
||||
|
||||
public void channelInactive(ChannelHandlerContext ctx) {
|
||||
EaglerPipeline.closeChannel(ctx.channel());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,233 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.PingWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.PongWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
|
||||
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.query.QueryManager;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 abstract class HttpServerQueryHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
public static class UnexpectedDataException extends RuntimeException {
|
||||
public UnexpectedDataException() {
|
||||
}
|
||||
public UnexpectedDataException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
public UnexpectedDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public UnexpectedDataException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static final InetAddress localhost;
|
||||
|
||||
static {
|
||||
try {
|
||||
localhost = InetAddress.getLocalHost();
|
||||
}catch(Throwable t) {
|
||||
throw new RuntimeException("localhost doesn't exist?!", t);
|
||||
}
|
||||
}
|
||||
|
||||
private EaglerListenerConfig conf;
|
||||
private ChannelHandlerContext context;
|
||||
private String accept;
|
||||
private boolean acceptTextPacket = false;
|
||||
private boolean acceptBinaryPacket = false;
|
||||
private boolean hasClosed = false;
|
||||
private boolean keepAlive = false;
|
||||
|
||||
public void beginHandleQuery(EaglerListenerConfig conf, ChannelHandlerContext context, String accept) {
|
||||
this.conf = conf;
|
||||
this.context = context;
|
||||
this.accept = accept;
|
||||
begin(accept);
|
||||
}
|
||||
|
||||
protected void acceptText() {
|
||||
acceptText(true);
|
||||
}
|
||||
|
||||
protected void acceptText(boolean bool) {
|
||||
acceptTextPacket = bool;
|
||||
}
|
||||
|
||||
protected void acceptBinary() {
|
||||
acceptBinary(true);
|
||||
}
|
||||
|
||||
protected void acceptBinary(boolean bool) {
|
||||
acceptBinaryPacket = bool;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
context.close();
|
||||
hasClosed = true;
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return hasClosed;
|
||||
}
|
||||
|
||||
public InetAddress getAddress() {
|
||||
InetAddress addr = context.channel().attr(EaglerPipeline.REAL_ADDRESS).get();
|
||||
if(addr != null) {
|
||||
return addr;
|
||||
}else {
|
||||
SocketAddress sockAddr = context.channel().remoteAddress();
|
||||
return sockAddr instanceof InetSocketAddress ? ((InetSocketAddress) sockAddr).getAddress() : localhost;
|
||||
}
|
||||
}
|
||||
|
||||
public ChannelHandlerContext getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public EaglerListenerConfig getListener() {
|
||||
return conf;
|
||||
}
|
||||
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
try {
|
||||
if (msg instanceof WebSocketFrame) {
|
||||
if (msg instanceof BinaryWebSocketFrame) {
|
||||
handleBinary(ctx, ((BinaryWebSocketFrame) msg).content());
|
||||
} else if (msg instanceof TextWebSocketFrame) {
|
||||
handleText(ctx, ((TextWebSocketFrame) msg).text());
|
||||
} else if (msg instanceof PingWebSocketFrame) {
|
||||
ctx.writeAndFlush(new PongWebSocketFrame());
|
||||
} else if (msg instanceof CloseWebSocketFrame) {
|
||||
ctx.close();
|
||||
}
|
||||
} else {
|
||||
EaglerXVelocity.logger().error("Unexpected Packet: {}", msg.getClass().getSimpleName());
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
if (ctx.channel().isActive()) {
|
||||
EaglerXVelocity.logger().warn("[{}]: Exception Caught: {}", ctx.channel().remoteAddress(), cause.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBinary(ChannelHandlerContext ctx, ByteBuf buffer) {
|
||||
if(!acceptBinaryPacket) {
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
byte[] packet = new byte[buffer.readableBytes()];
|
||||
buffer.readBytes(packet);
|
||||
processBytes(packet);
|
||||
}
|
||||
|
||||
private void handleText(ChannelHandlerContext ctx, String str) {
|
||||
if(!acceptTextPacket) {
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
JsonObject obj = null;
|
||||
if(str.indexOf('{') == 0) {
|
||||
try {
|
||||
obj = JsonParser.parseString(str).getAsJsonObject();
|
||||
}catch(JsonParseException ex) {
|
||||
}
|
||||
}
|
||||
if(obj != null) {
|
||||
processJson(obj);
|
||||
}else {
|
||||
processString(str);
|
||||
}
|
||||
}
|
||||
|
||||
public void channelInactive(ChannelHandlerContext ctx) {
|
||||
EaglerPipeline.closeChannel(ctx.channel());
|
||||
hasClosed = true;
|
||||
closed();
|
||||
}
|
||||
|
||||
public String getAccept() {
|
||||
return accept;
|
||||
}
|
||||
|
||||
public void sendStringResponse(String type, String str) {
|
||||
context.writeAndFlush(new TextWebSocketFrame(QueryManager.createStringResponse(accept, str).toString()));
|
||||
}
|
||||
|
||||
public void sendStringResponseAndClose(String type, String str) {
|
||||
context.writeAndFlush(new TextWebSocketFrame(QueryManager.createStringResponse(accept, str).toString())).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
public void sendJsonResponse(String type, JsonObject obj) {
|
||||
context.writeAndFlush(new TextWebSocketFrame(QueryManager.createJsonObjectResponse(accept, obj).toString()));
|
||||
}
|
||||
|
||||
public void sendJsonResponseAndClose(String type, JsonObject obj) {
|
||||
context.writeAndFlush(new TextWebSocketFrame(QueryManager.createJsonObjectResponse(accept, obj).toString())).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
public void sendBinaryResponse(byte[] bytes) {
|
||||
context.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(bytes)));
|
||||
}
|
||||
|
||||
public void sendBinaryResponseAndClose(byte[] bytes) {
|
||||
context.writeAndFlush(new BinaryWebSocketFrame(Unpooled.wrappedBuffer(bytes))).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
|
||||
public void setKeepAlive(boolean enable) {
|
||||
keepAlive = enable;
|
||||
}
|
||||
|
||||
public boolean shouldKeepAlive() {
|
||||
return keepAlive;
|
||||
}
|
||||
|
||||
protected abstract void begin(String queryType);
|
||||
|
||||
protected abstract void processString(String str);
|
||||
|
||||
protected abstract void processJson(JsonObject obj);
|
||||
|
||||
protected abstract void processBytes(byte[] bytes);
|
||||
|
||||
protected abstract void closed();
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,227 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.query.EaglerQuerySimpleHandler;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.query.MOTDConnection;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerListenerConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.MOTDCacheConfiguration;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 MOTDQueryHandler extends EaglerQuerySimpleHandler implements MOTDConnection {
|
||||
|
||||
private long creationTime = 0l;
|
||||
|
||||
private String line1;
|
||||
private String line2;
|
||||
private List<String> players;
|
||||
private int[] bitmap;
|
||||
private int onlinePlayers;
|
||||
private int maxPlayers;
|
||||
private boolean hasIcon;
|
||||
private boolean iconDirty;
|
||||
private String subType;
|
||||
private String returnType;
|
||||
|
||||
@Override
|
||||
protected void begin(String queryType) {
|
||||
creationTime = System.currentTimeMillis();
|
||||
subType = queryType;
|
||||
returnType = "MOTD";
|
||||
EaglerListenerConfig listener = getListener();
|
||||
List<String> lns = listener.getServerMOTD();
|
||||
if(lns.size() >= 1) {
|
||||
line1 = lns.get(0);
|
||||
}
|
||||
if(lns.size() >= 2) {
|
||||
line2 = lns.get(1);
|
||||
}
|
||||
maxPlayers = listener.getMaxPlayer();
|
||||
onlinePlayers = EaglerXVelocity.proxy().getPlayerCount();
|
||||
players = new ArrayList();
|
||||
for(Player pp : EaglerXVelocity.proxy().getAllPlayers()) {
|
||||
players.add(pp.getUsername());
|
||||
if(players.size() >= 9) {
|
||||
players.add("\u00A77\u00A7o(" + (onlinePlayers - players.size()) + " more)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
bitmap = new int[4096];
|
||||
int i = queryType.indexOf('.');
|
||||
if(i > 0) {
|
||||
subType = queryType.substring(i + 1);
|
||||
if(subType.length() == 0) {
|
||||
subType = "motd";
|
||||
}
|
||||
}else {
|
||||
subType = "motd";
|
||||
}
|
||||
if(!subType.startsWith("noicon") && !subType.startsWith("cache.noicon")) {
|
||||
int[] maybeIcon = listener.getServerIconPixels();
|
||||
iconDirty = hasIcon = maybeIcon != null;
|
||||
if(hasIcon) {
|
||||
System.arraycopy(maybeIcon, 0, bitmap, 0, 4096);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getConnectionTimestamp() {
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToUser() {
|
||||
if(!isClosed()) {
|
||||
JsonObject obj = new JsonObject();
|
||||
if(subType.startsWith("cache.anim")) {
|
||||
obj.addProperty("unsupported", true);
|
||||
sendJsonResponseAndClose(returnType, obj);
|
||||
return;
|
||||
}else if(subType.startsWith("cache")) {
|
||||
JsonArray cacheControl = new JsonArray();
|
||||
MOTDCacheConfiguration cc = getListener().getMOTDCacheConfig();
|
||||
if(cc.cacheServerListAnimation) {
|
||||
cacheControl.add("animation");
|
||||
}
|
||||
if(cc.cacheServerListResults) {
|
||||
cacheControl.add("results");
|
||||
}
|
||||
if(cc.cacheServerListTrending) {
|
||||
cacheControl.add("trending");
|
||||
}
|
||||
if(cc.cacheServerListPortfolios) {
|
||||
cacheControl.add("portfolio");
|
||||
}
|
||||
obj.add("cache", cacheControl);
|
||||
obj.addProperty("ttl", cc.cacheTTL);
|
||||
}else {
|
||||
MOTDCacheConfiguration cc = getListener().getMOTDCacheConfig();;
|
||||
obj.addProperty("cache", cc.cacheServerListAnimation || cc.cacheServerListResults ||
|
||||
cc.cacheServerListTrending || cc.cacheServerListPortfolios);
|
||||
}
|
||||
boolean noIcon = subType.startsWith("noicon") || subType.startsWith("cache.noicon");
|
||||
JsonArray motd = new JsonArray();
|
||||
if(line1 != null && line1.length() > 0) motd.add(line1);
|
||||
if(line2 != null && line2.length() > 0) motd.add(line2);
|
||||
obj.add("motd", motd);
|
||||
obj.addProperty("icon", hasIcon && !noIcon);
|
||||
obj.addProperty("online", onlinePlayers);
|
||||
obj.addProperty("max", maxPlayers);
|
||||
JsonArray playerz = new JsonArray();
|
||||
for(String s : players) {
|
||||
playerz.add(s);
|
||||
}
|
||||
obj.add("players", playerz);
|
||||
sendJsonResponse(returnType, obj);
|
||||
if(hasIcon && !noIcon && iconDirty && bitmap != null) {
|
||||
byte[] iconPixels = new byte[16384];
|
||||
for(int i = 0, j; i < 4096; ++i) {
|
||||
j = i << 2;
|
||||
iconPixels[j] = (byte)((bitmap[i] >> 16) & 0xFF);
|
||||
iconPixels[j + 1] = (byte)((bitmap[i] >> 8) & 0xFF);
|
||||
iconPixels[j + 2] = (byte)(bitmap[i] & 0xFF);
|
||||
iconPixels[j + 3] = (byte)((bitmap[i] >> 24) & 0xFF);
|
||||
}
|
||||
sendBinaryResponse(iconPixels);
|
||||
iconDirty = false;
|
||||
}
|
||||
if(subType.startsWith("cache")) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLine1() {
|
||||
return line1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLine2() {
|
||||
return line2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPlayerList() {
|
||||
return players;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getBitmap() {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOnlinePlayers() {
|
||||
return onlinePlayers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxPlayers() {
|
||||
return maxPlayers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSubType() {
|
||||
return subType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLine1(String p) {
|
||||
line1 = p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLine2(String p) {
|
||||
line2 = p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayerList(List<String> p) {
|
||||
players = p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlayerList(String... p) {
|
||||
players = Arrays.asList(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBitmap(int[] p) {
|
||||
iconDirty = hasIcon = true;
|
||||
bitmap = p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnlinePlayers(int i) {
|
||||
onlinePlayers = i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxPlayers(int i) {
|
||||
maxPlayers = i;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.query;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocityVersion;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerVelocityConfig;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.HttpServerQueryHandler;
|
||||
|
||||
/**
|
||||
* 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 QueryManager {
|
||||
|
||||
private static final Map<String, Class<? extends HttpServerQueryHandler>> queryTypes = new HashMap();
|
||||
|
||||
static {
|
||||
queryTypes.put("motd", MOTDQueryHandler.class);
|
||||
queryTypes.put("motd.cache", MOTDQueryHandler.class);
|
||||
queryTypes.put("version", VersionQueryHandler.class);
|
||||
}
|
||||
|
||||
public static HttpServerQueryHandler createQueryHandler(String type) {
|
||||
Class<? extends HttpServerQueryHandler> clazz;
|
||||
synchronized(queryTypes) {
|
||||
clazz = queryTypes.get(type);
|
||||
}
|
||||
if(clazz != null) {
|
||||
HttpServerQueryHandler obj = null;
|
||||
try {
|
||||
obj = clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
EaglerXVelocity.logger().error("Exception creating query handler for '" + type + "'!", e);
|
||||
}
|
||||
if(obj != null) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void registerQueryType(String name, Class<? extends HttpServerQueryHandler> clazz) {
|
||||
synchronized(queryTypes) {
|
||||
if(queryTypes.put(name, clazz) != null) {
|
||||
EaglerXVelocity.logger().warn("Query type '" + name + "' was registered twice, probably by two different plugins!");
|
||||
Thread.dumpStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void unregisterQueryType(String name) {
|
||||
synchronized(queryTypes) {
|
||||
queryTypes.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonObject createBaseResponse() {
|
||||
EaglerXVelocity plugin = EaglerXVelocity.getEagler();
|
||||
EaglerVelocityConfig conf = plugin.getConfig();
|
||||
JsonObject json = new JsonObject();
|
||||
json.addProperty("name", conf.getServerName());
|
||||
json.addProperty("brand", "lax1dude");
|
||||
json.addProperty("vers", EaglerXVelocityVersion.ID + "/" + EaglerXVelocityVersion.VERSION);
|
||||
json.addProperty("cracked", conf.isCracked());
|
||||
json.addProperty("secure", false);
|
||||
json.addProperty("time", System.currentTimeMillis());
|
||||
json.addProperty("uuid", conf.getServerUUID().toString());
|
||||
return json;
|
||||
}
|
||||
|
||||
public static JsonObject createStringResponse(String type, String str) {
|
||||
JsonObject ret = createBaseResponse();
|
||||
ret.addProperty("type", type);
|
||||
ret.addProperty("data", str);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static JsonObject createJsonObjectResponse(String type, JsonObject json) {
|
||||
JsonObject ret = createBaseResponse();
|
||||
ret.addProperty("type", type);
|
||||
ret.add("data", json);
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.query;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.api.query.EaglerQuerySimpleHandler;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 VersionQueryHandler extends EaglerQuerySimpleHandler {
|
||||
|
||||
@Override
|
||||
protected void begin(String queryType) {
|
||||
JsonObject responseObj = new JsonObject();
|
||||
JsonArray handshakeVersions = new JsonArray();
|
||||
handshakeVersions.add(2);
|
||||
handshakeVersions.add(3);
|
||||
responseObj.add("handshakeVersions", handshakeVersions);
|
||||
JsonArray protocolVersions = new JsonArray();
|
||||
protocolVersions.add(47);
|
||||
responseObj.add("protocolVersions", protocolVersions);
|
||||
JsonArray gameVersions = new JsonArray();
|
||||
gameVersions.add("1.8");
|
||||
responseObj.add("gameVersions", gameVersions);
|
||||
JsonObject proxyInfo = new JsonObject();
|
||||
proxyInfo.addProperty("brand", EaglerXVelocity.proxy().getVersion().getName());
|
||||
proxyInfo.addProperty("vers", EaglerXVelocity.proxy().getVersion().getVersion());
|
||||
responseObj.add("proxyVersions", proxyInfo);
|
||||
sendJsonResponseAndClose("version", responseObj);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 HttpContentType {
|
||||
|
||||
public final Set<String> extensions;
|
||||
public final String mimeType;
|
||||
public final String charset;
|
||||
public final String httpHeader;
|
||||
public final String cacheControlHeader;
|
||||
public final long fileBrowserCacheTTL;
|
||||
|
||||
public static final HttpContentType defaultType = new HttpContentType(new HashSet(), "application/octet-stream", null, 14400000l);
|
||||
|
||||
public HttpContentType(Set<String> extensions, String mimeType, String charset, long fileBrowserCacheTTL) {
|
||||
this.extensions = extensions;
|
||||
this.mimeType = mimeType;
|
||||
this.charset = charset;
|
||||
this.fileBrowserCacheTTL = fileBrowserCacheTTL;
|
||||
if(charset == null) {
|
||||
this.httpHeader = mimeType;
|
||||
}else {
|
||||
this.httpHeader = mimeType + "; charset=" + charset;
|
||||
}
|
||||
if(fileBrowserCacheTTL > 0l) {
|
||||
this.cacheControlHeader = "max-age=" + (fileBrowserCacheTTL / 1000l);
|
||||
}else {
|
||||
this.cacheControlHeader = "no-cache";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.SimpleTimeZone;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpResponse;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaders;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocityVersion;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 HttpMemoryCache {
|
||||
|
||||
public File fileObject;
|
||||
public String filePath;
|
||||
public ByteBuf fileData;
|
||||
public HttpContentType contentType;
|
||||
public long lastCacheHit;
|
||||
public long lastDiskReload;
|
||||
public long lastDiskModified;
|
||||
private final String server;
|
||||
|
||||
private static final SimpleDateFormat gmt;
|
||||
|
||||
static {
|
||||
gmt = new SimpleDateFormat();
|
||||
gmt.setTimeZone(new SimpleTimeZone(0, "GMT"));
|
||||
gmt.applyPattern("dd MMM yyyy HH:mm:ss z");
|
||||
}
|
||||
|
||||
public HttpMemoryCache(File fileObject, String filePath, ByteBuf fileData, HttpContentType contentType,
|
||||
long lastCacheHit, long lastDiskReload, long lastDiskModified) {
|
||||
this.fileObject = fileObject;
|
||||
this.filePath = filePath;
|
||||
this.fileData = fileData;
|
||||
this.contentType = contentType;
|
||||
this.lastCacheHit = lastCacheHit;
|
||||
this.lastDiskReload = lastDiskReload;
|
||||
this.lastDiskModified = lastDiskModified;
|
||||
this.server = EaglerXVelocityVersion.ID + "/" + EaglerXVelocityVersion.VERSION;
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse createHTTPResponse() {
|
||||
return createHTTPResponse(HttpResponseStatus.OK);
|
||||
}
|
||||
|
||||
public DefaultFullHttpResponse createHTTPResponse(HttpResponseStatus code) {
|
||||
DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, code, Unpooled.wrappedBuffer(fileData.retain()));
|
||||
HttpHeaders responseHeaders = response.headers();
|
||||
Date d = new Date();
|
||||
responseHeaders.add(HttpHeaderNames.CONTENT_TYPE, contentType.httpHeader);
|
||||
responseHeaders.add(HttpHeaderNames.CONTENT_LENGTH, fileData.readableBytes());
|
||||
responseHeaders.add(HttpHeaderNames.CACHE_CONTROL, contentType.cacheControlHeader);
|
||||
responseHeaders.add(HttpHeaderNames.DATE, gmt.format(d));
|
||||
long l = contentType.fileBrowserCacheTTL;
|
||||
if(l > 0l && l != Long.MAX_VALUE) {
|
||||
d.setTime(d.getTime() + l);
|
||||
responseHeaders.add(HttpHeaderNames.EXPIRES, gmt.format(d));
|
||||
}
|
||||
d.setTime(lastDiskModified);
|
||||
responseHeaders.add(HttpHeaderNames.LAST_MODIFIED, gmt.format(d));
|
||||
responseHeaders.add(HttpHeaderNames.SERVER, server);
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.web;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocityVersion;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 HttpWebServer {
|
||||
|
||||
public final File directory;
|
||||
public final Map<String,HttpContentType> contentTypes;
|
||||
private final Map<String,HttpMemoryCache> filesCache;
|
||||
private final List<String> index;
|
||||
private final String page404;
|
||||
private static HttpMemoryCache default404Page;
|
||||
private static HttpMemoryCache default404UpgradePage;
|
||||
private static final Object cacheClearLock = new Object();
|
||||
|
||||
public HttpWebServer(File directory, Map<String,HttpContentType> contentTypes, List<String> index, String page404) {
|
||||
this.directory = directory;
|
||||
this.contentTypes = contentTypes;
|
||||
this.filesCache = new HashMap();
|
||||
this.index = index;
|
||||
this.page404 = page404;
|
||||
}
|
||||
|
||||
public void flushCache() {
|
||||
long millis = System.currentTimeMillis();
|
||||
synchronized(cacheClearLock) {
|
||||
synchronized(filesCache) {
|
||||
Iterator<HttpMemoryCache> itr = filesCache.values().iterator();
|
||||
while(itr.hasNext()) {
|
||||
HttpMemoryCache i = itr.next();
|
||||
if(i.contentType.fileBrowserCacheTTL != Long.MAX_VALUE && millis - i.lastCacheHit > 900000l) {
|
||||
i.fileData.release();
|
||||
itr.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HttpMemoryCache retrieveFile(String path) {
|
||||
try {
|
||||
String[] pathSplit = path.split("(\\\\|\\/)+");
|
||||
|
||||
List<String> pathList = pathSplit.length == 0 ? null : new ArrayList();
|
||||
for(int i = 0; i < pathSplit.length; ++i) {
|
||||
pathSplit[i] = pathSplit[i].trim();
|
||||
if(pathSplit[i].length() > 0) {
|
||||
if(!pathSplit[i].equals(".") && !pathSplit[i].startsWith("..")) {
|
||||
pathList.add(pathSplit[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpMemoryCache cached;
|
||||
|
||||
if(pathList == null || pathList.size() == 0) {
|
||||
for(int i = 0, l = index.size(); i < l; ++i) {
|
||||
cached = retrieveFile(index.get(i));
|
||||
if(cached != null) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String joinedPath = String.join("/", pathList);
|
||||
|
||||
synchronized(cacheClearLock) {
|
||||
synchronized(filesCache) {
|
||||
cached = filesCache.get(joinedPath);
|
||||
}
|
||||
|
||||
if(cached != null) {
|
||||
cached = validateCache(cached);
|
||||
if(cached != null) {
|
||||
return cached;
|
||||
}else {
|
||||
synchronized(filesCache) {
|
||||
filesCache.remove(joinedPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File f = new File(directory, joinedPath);
|
||||
|
||||
if(!f.exists()) {
|
||||
if(page404 == null || path.equals(page404)) {
|
||||
return default404Page;
|
||||
}else {
|
||||
return retrieveFile(page404);
|
||||
}
|
||||
}
|
||||
|
||||
if(f.isDirectory()) {
|
||||
for(int i = 0, l = index.size(); i < l; ++i) {
|
||||
String p = joinedPath + "/" + index.get(i);
|
||||
synchronized(filesCache) {
|
||||
cached = filesCache.get(p);
|
||||
}
|
||||
if(cached != null) {
|
||||
cached = validateCache(cached);
|
||||
if(cached != null) {
|
||||
synchronized(filesCache) {
|
||||
filesCache.put(joinedPath, cached);
|
||||
}
|
||||
}else {
|
||||
synchronized(filesCache) {
|
||||
filesCache.remove(p);
|
||||
}
|
||||
if(page404 == null || path.equals(page404)) {
|
||||
return default404Page;
|
||||
}else {
|
||||
return retrieveFile(page404);
|
||||
}
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
for(int i = 0, l = index.size(); i < l; ++i) {
|
||||
String p = joinedPath + "/" + index.get(i);
|
||||
File ff = new File(directory, p);
|
||||
if(ff.isFile()) {
|
||||
HttpMemoryCache memCache = retrieveFile(ff, p);
|
||||
if(memCache != null) {
|
||||
synchronized(filesCache) {
|
||||
filesCache.put(joinedPath, memCache);
|
||||
}
|
||||
return memCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(page404 == null || path.equals(page404)) {
|
||||
return default404Page;
|
||||
}else {
|
||||
return retrieveFile(page404);
|
||||
}
|
||||
}else {
|
||||
HttpMemoryCache memCache = retrieveFile(f, joinedPath);
|
||||
if(memCache != null) {
|
||||
synchronized(filesCache) {
|
||||
filesCache.put(joinedPath, memCache);
|
||||
}
|
||||
return memCache;
|
||||
}else {
|
||||
if(page404 == null || path.equals(page404)) {
|
||||
return default404Page;
|
||||
}else {
|
||||
return retrieveFile(page404);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(Throwable t) {
|
||||
return default404Page;
|
||||
}
|
||||
}
|
||||
|
||||
private HttpMemoryCache retrieveFile(File path, String requestCachePath) {
|
||||
int fileSize = (int)path.length();
|
||||
try(FileInputStream is = new FileInputStream(path)) {
|
||||
ByteBuf file = Unpooled.buffer(fileSize, fileSize);
|
||||
file.writeBytes(is, fileSize);
|
||||
String ext = path.getName();
|
||||
HttpContentType ct = null;
|
||||
int i = ext.lastIndexOf('.');
|
||||
if(i != -1) {
|
||||
ct = contentTypes.get(ext.substring(i + 1));
|
||||
}
|
||||
if(ct == null) {
|
||||
ct = HttpContentType.defaultType;
|
||||
}
|
||||
long millis = System.currentTimeMillis();
|
||||
return new HttpMemoryCache(path, requestCachePath, file, ct, millis, millis, path.lastModified());
|
||||
}catch(Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private HttpMemoryCache validateCache(HttpMemoryCache file) {
|
||||
if(file.fileObject == null) {
|
||||
return file;
|
||||
}
|
||||
long millis = System.currentTimeMillis();
|
||||
file.lastCacheHit = millis;
|
||||
if(millis - file.lastDiskReload > 4000l) {
|
||||
File f = file.fileObject;
|
||||
if(!f.isFile()) {
|
||||
return null;
|
||||
}else {
|
||||
long lastMod = f.lastModified();
|
||||
if(lastMod != file.lastDiskModified) {
|
||||
int fileSize = (int)f.length();
|
||||
try(FileInputStream is = new FileInputStream(f)) {
|
||||
file.fileData = Unpooled.buffer(fileSize, fileSize);
|
||||
file.fileData.writeBytes(is, fileSize);
|
||||
file.lastDiskReload = millis;
|
||||
file.lastDiskModified = lastMod;
|
||||
return file;
|
||||
}catch(Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}else {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}else {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
public static void regenerate404Pages() {
|
||||
if(default404Page != null) {
|
||||
default404Page.fileData.release();
|
||||
}
|
||||
default404Page = regenerateDefault404();
|
||||
if(default404UpgradePage != null) {
|
||||
default404UpgradePage.fileData.release();
|
||||
}
|
||||
default404UpgradePage = regenerateDefaultUpgrade404();
|
||||
}
|
||||
|
||||
public static HttpMemoryCache getHTTP404() {
|
||||
return default404Page;
|
||||
}
|
||||
|
||||
public static HttpMemoryCache getWebSocket404() {
|
||||
return default404UpgradePage;
|
||||
}
|
||||
|
||||
private static HttpMemoryCache regenerateDefault404() {
|
||||
EaglerXVelocity plugin = EaglerXVelocity.getEagler();
|
||||
byte[] src = ("<!DOCTYPE html><html><head><title>" + htmlEntities(plugin.getConfig().getServerName()) + "</title><script type=\"text/javascript\">"
|
||||
+ "window.addEventListener(\"load\",()=>document.getElementById(\"addr\").innerText=window.location.href);</script></head>"
|
||||
+ "<body style=\"font-family:sans-serif;text-align:center;\"><h1>404 Not Found</h1><hr /><p style=\"font-size:1.2em;\">"
|
||||
+ "The requested resource <span id=\"addr\" style=\"font-family:monospace;font-weight:bold;background-color:#EEEEEE;padding:3px 4px;\">"
|
||||
+ "</span> could not be found on this server!</p><p>" + htmlEntities(EaglerXVelocityVersion.NAME) + "/"
|
||||
+ htmlEntities(EaglerXVelocityVersion.VERSION) + "</p></body></html>").getBytes(StandardCharsets.UTF_8);
|
||||
HttpContentType htmlContentType = new HttpContentType(new HashSet(Arrays.asList("html")), "text/html", "utf-8", 120000l);
|
||||
long millis = System.currentTimeMillis();
|
||||
return new HttpMemoryCache(null, "~404", Unpooled.wrappedBuffer(src), htmlContentType, millis, millis, millis);
|
||||
}
|
||||
|
||||
private static HttpMemoryCache regenerateDefaultUpgrade404() {
|
||||
EaglerXVelocity plugin = EaglerXVelocity.getEagler();
|
||||
String name = htmlEntities(plugin.getConfig().getServerName());
|
||||
byte[] src = ("<!DOCTYPE html><html><head><meta charset=\"UTF-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" /><title>" + name +
|
||||
"</title><script type=\"text/javascript\">window.addEventListener(\"load\",()=>{var src=window.location.href;const gEI=(i)=>document.getElementById(i);"
|
||||
+ "if(src.startsWith(\"http:\")){src=\"ws:\"+src.substring(5);}else if(src.startsWith(\"https:\")){src=\"wss:\"+src.substring(6);}else{return;}"
|
||||
+ "gEI(\"wsUri\").innerHTML=\"<span id=\\\"wsField\\\" style=\\\"font-family:monospace;font-weight:bold;background-color:#EEEEEE;padding:3px 4px;\\\">"
|
||||
+ "</span>\";gEI(\"wsField\").innerText=src;});</script></head><body style=\"font-family:sans-serif;margin:0px;padding:12px;\"><h1 style=\"margin-block-start:0px;\">"
|
||||
+ "404 'Websocket Upgrade Failure' (rip)</h1><h3>The URL you have requested is the physical WebSocket address of '" + name + "'</h3><p style=\"font-size:1.2em;"
|
||||
+ "line-height:1.3em;\">To correctly join this server, load the latest EaglercraftX 1.8 client, click the 'Direct Connect' button<br />on the 'Multiplayer' screen, "
|
||||
+ "and enter <span id=\"wsUri\">this URL</span> as the server address</p></body></html>").getBytes(StandardCharsets.UTF_8);
|
||||
HttpContentType htmlContentType = new HttpContentType(new HashSet(Arrays.asList("html")), "text/html", "utf-8", 14400000l);
|
||||
long millis = System.currentTimeMillis();
|
||||
return new HttpMemoryCache(null, "~404", Unpooled.wrappedBuffer(src), htmlContentType, millis, millis, millis);
|
||||
}
|
||||
|
||||
public static String htmlEntities(String input) {
|
||||
return input.replace("<", "<").replace(">", ">");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.shit;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocityVersion;
|
||||
|
||||
/**
|
||||
* 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 CompatWarning {
|
||||
|
||||
public static void displayCompatWarning() {
|
||||
String stfu = System.getProperty("eaglerxvelocity.stfu");
|
||||
if("true".equalsIgnoreCase(stfu)) {
|
||||
return;
|
||||
}
|
||||
String[] compatWarnings = new String[] {
|
||||
":>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>",
|
||||
":> ",
|
||||
":> EAGLERCRAFTXVELOCITY WARNING:",
|
||||
":> ",
|
||||
":> This plugin wasn\'t tested to be \'working\'",
|
||||
":> with ANY version of Velocity (and forks)",
|
||||
":> apart from the versions listed below:",
|
||||
":> ",
|
||||
":> - Velocity: " + EaglerXVelocityVersion.NATIVE_VELOCITY_BUILD,
|
||||
":> ",
|
||||
":> This is not a Bukkit/Spigot plugin!",
|
||||
":> ",
|
||||
":> Use \"-Deaglerxvelocity.stfu=true\" to hide",
|
||||
":> ",
|
||||
":>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>"
|
||||
};
|
||||
for(int i = 0; i < compatWarnings.length; ++i) {
|
||||
System.err.println(compatWarnings[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.shit;
|
||||
|
||||
import java.awt.GraphicsEnvironment;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
|
||||
/**
|
||||
* 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 MainClass {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.err.println();
|
||||
System.err.println("ERROR: The EaglerXVelocity 1.8 jar file is a PLUGIN intended to be used with Velocity!");
|
||||
System.err.println("Place this file in the \"plugins\" directory of your Velocity installation");
|
||||
System.err.println();
|
||||
try {
|
||||
tryShowPopup();
|
||||
}catch(Throwable t) {
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private static void tryShowPopup() throws Throwable {
|
||||
if(!GraphicsEnvironment.isHeadless()) {
|
||||
JOptionPane.showMessageDialog(null, "ERROR: The EaglerXVelocity 1.8 jar file is a PLUGIN intended to be used with Velocity!\nPlace this file in the \"plugins\" directory of your Velocity installation", "EaglerXVelocity", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,452 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.BinaryHttpClient.Response;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.ICacheProvider.CacheLoadedProfile;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins.ICacheProvider.CacheLoadedSkin;
|
||||
|
||||
/**
|
||||
* 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 AsyncSkinProvider {
|
||||
|
||||
private static class SkinConsumerImpl implements Consumer<Response> {
|
||||
|
||||
protected final Consumer<byte[]> responseConsumer;
|
||||
|
||||
protected SkinConsumerImpl(Consumer<byte[]> consumer) {
|
||||
this.responseConsumer = consumer;
|
||||
}
|
||||
|
||||
protected void doAccept(byte[] v) {
|
||||
try {
|
||||
responseConsumer.accept(v);
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown caching new skin!", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Response response) {
|
||||
if(response == null || response.exception != null || response.code != 200 || response.data == null) {
|
||||
doAccept(null);
|
||||
}else {
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = ImageIO.read(new ByteArrayInputStream(response.data));
|
||||
}catch(IOException ex) {
|
||||
doAccept(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int srcWidth = image.getWidth();
|
||||
int srcHeight = image.getHeight();
|
||||
if(srcWidth < 64 || srcWidth > 512 || srcHeight < 32 || srcHeight > 512) {
|
||||
doAccept(null);
|
||||
return;
|
||||
}
|
||||
if(srcWidth != 64 || srcHeight != 64) {
|
||||
if(srcWidth % 64 == 0) {
|
||||
if(srcWidth == srcHeight * 2) {
|
||||
BufferedImage scaled = new BufferedImage(64, 32, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = scaled.createGraphics();
|
||||
graphics.drawImage(image, 0, 0, 64, 32, 0, 0, srcWidth, srcHeight, null);
|
||||
graphics.dispose();
|
||||
image = scaled;
|
||||
srcWidth = 64;
|
||||
srcHeight = 32;
|
||||
}else if(srcWidth == srcHeight) {
|
||||
BufferedImage scaled = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics = scaled.createGraphics();
|
||||
graphics.drawImage(image, 0, 0, 64, 64, 0, 0, srcWidth, srcHeight, null);
|
||||
graphics.dispose();
|
||||
image = scaled;
|
||||
srcWidth = 64;
|
||||
srcHeight = 64;
|
||||
}else {
|
||||
doAccept(null);
|
||||
return;
|
||||
}
|
||||
}else {
|
||||
doAccept(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(srcWidth == 64 && srcHeight == 64) {
|
||||
int[] tmp = new int[4096];
|
||||
byte[] loadedPixels = new byte[16384];
|
||||
image.getRGB(0, 0, 64, 64, tmp, 0, 64);
|
||||
SkinRescaler.convertToBytes(tmp, loadedPixels);
|
||||
SkinPackets.setAlphaForChest(loadedPixels, (byte)255, 0);
|
||||
doAccept(loadedPixels);
|
||||
return;
|
||||
}else if(srcWidth == 64 && srcHeight == 32) {
|
||||
int[] tmp1 = new int[2048];
|
||||
byte[] loadedPixels = new byte[16384];
|
||||
image.getRGB(0, 0, 64, 32, tmp1, 0, 64);
|
||||
SkinRescaler.convert64x32To64x64(tmp1, loadedPixels);
|
||||
SkinPackets.setAlphaForChest(loadedPixels, (byte)255, 0);
|
||||
doAccept(loadedPixels);
|
||||
return;
|
||||
}else {
|
||||
doAccept(null);
|
||||
return;
|
||||
}
|
||||
}catch(Throwable t) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class SkinCachingConsumer implements Consumer<byte[]> {
|
||||
|
||||
protected final UUID skinUUID;
|
||||
protected final String skinTexture;
|
||||
protected final ICacheProvider cacheProvider;
|
||||
protected final Consumer<byte[]> responseConsumer;
|
||||
|
||||
protected SkinCachingConsumer(UUID skinUUID, String skinTexture, ICacheProvider cacheProvider,
|
||||
Consumer<byte[]> responseConsumer) {
|
||||
this.skinUUID = skinUUID;
|
||||
this.skinTexture = skinTexture;
|
||||
this.cacheProvider = cacheProvider;
|
||||
this.responseConsumer = responseConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(byte[] skin) {
|
||||
if(skin != null) {
|
||||
try {
|
||||
cacheProvider.cacheSkinByUUID(skinUUID, skinTexture, skin);
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown writing new skin to database!", t);
|
||||
}
|
||||
responseConsumer.accept(skin);
|
||||
}else {
|
||||
responseConsumer.accept(null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CacheFetchedProfile {
|
||||
|
||||
public final UUID uuid;
|
||||
public final String username;
|
||||
public final String texture;
|
||||
public final UUID textureUUID;
|
||||
public final String model;
|
||||
|
||||
protected CacheFetchedProfile(UUID uuid, String username, String texture, String model) {
|
||||
this.uuid = uuid;
|
||||
this.username = username;
|
||||
this.texture = texture;
|
||||
this.textureUUID = SkinPackets.createEaglerURLSkinUUID(texture);
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
protected CacheFetchedProfile(CacheLoadedProfile profile) {
|
||||
this.uuid = profile.uuid;
|
||||
this.username = profile.username;
|
||||
this.texture = profile.texture;
|
||||
this.textureUUID = SkinPackets.createEaglerURLSkinUUID(profile.texture);
|
||||
this.model = profile.model;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ProfileConsumerImpl implements Consumer<Response> {
|
||||
|
||||
protected final UUID uuid;
|
||||
protected final Consumer<CacheFetchedProfile> responseConsumer;
|
||||
|
||||
protected ProfileConsumerImpl(UUID uuid, Consumer<CacheFetchedProfile> responseConsumer) {
|
||||
this.uuid = uuid;
|
||||
this.responseConsumer = responseConsumer;
|
||||
}
|
||||
|
||||
protected void doAccept(CacheFetchedProfile v) {
|
||||
try {
|
||||
responseConsumer.accept(v);
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown caching new profile!", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Response response) {
|
||||
if(response == null || response.exception != null || response.code != 200 || response.data == null) {
|
||||
doAccept(null);
|
||||
}else {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(new String(response.data, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
String username = json.get("name").getAsString().toLowerCase();
|
||||
String texture = null;
|
||||
String model = null;
|
||||
JsonElement propsElement = json.get("properties");
|
||||
if(propsElement != null) {
|
||||
try {
|
||||
JsonArray properties = propsElement.getAsJsonArray();
|
||||
if(properties.size() > 0) {
|
||||
for(int i = 0, l = properties.size(); i < l; ++i) {
|
||||
JsonElement prop = properties.get(i);
|
||||
if(prop.isJsonObject()) {
|
||||
JsonObject propObj = prop.getAsJsonObject();
|
||||
if(propObj.get("name").getAsString().equals("textures")) {
|
||||
String value = new String(Base64.decodeBase64(propObj.get("value").getAsString()), StandardCharsets.UTF_8);
|
||||
JsonObject texturesJson = JsonParser.parseString(value).getAsJsonObject();
|
||||
if(texturesJson != null && texturesJson.has("textures")) {
|
||||
texturesJson = texturesJson.getAsJsonObject("textures");
|
||||
JsonElement skin = texturesJson.get("SKIN");
|
||||
if(skin != null) {
|
||||
model = "default";
|
||||
JsonObject skinObj = skin.getAsJsonObject();
|
||||
JsonElement urlElement = skinObj.get("url");
|
||||
if(urlElement != null && !urlElement.isJsonNull()) {
|
||||
texture = urlElement.getAsString();
|
||||
}
|
||||
JsonElement metaElement = skinObj.get("metadata");
|
||||
if(metaElement != null) {
|
||||
JsonObject metaObj = metaElement.getAsJsonObject();
|
||||
JsonElement modelElement = metaObj.get("model");
|
||||
if(modelElement != null) {
|
||||
model = modelElement.getAsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(Throwable t2) {
|
||||
}
|
||||
}
|
||||
if(texture == null && model == null) {
|
||||
model = SkinService.isAlex(uuid) ? "slim" : "default";
|
||||
}
|
||||
doAccept(new CacheFetchedProfile(uuid, username, texture, model));
|
||||
}catch(Throwable ex) {
|
||||
doAccept(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ProfileCachingConsumer implements Consumer<CacheFetchedProfile> {
|
||||
|
||||
protected final UUID uuid;
|
||||
protected final ICacheProvider cacheProvider;
|
||||
protected final Consumer<CacheFetchedProfile> responseConsumer;
|
||||
|
||||
protected ProfileCachingConsumer(UUID uuid, ICacheProvider cacheProvider, Consumer<CacheFetchedProfile> responseConsumer) {
|
||||
this.uuid = uuid;
|
||||
this.cacheProvider = cacheProvider;
|
||||
this.responseConsumer = responseConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(CacheFetchedProfile profile) {
|
||||
if(profile != null) {
|
||||
try {
|
||||
cacheProvider.cacheProfileByUUID(uuid, profile.username, profile.texture, profile.model);
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown writing new profile to database!", t);
|
||||
}
|
||||
responseConsumer.accept(profile);
|
||||
}else {
|
||||
responseConsumer.accept(null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class UsernameToUUIDConsumerImpl implements Consumer<Response> {
|
||||
|
||||
protected final String username;
|
||||
protected final ICacheProvider cacheProvider;
|
||||
protected final Consumer<CacheFetchedProfile> responseConsumer;
|
||||
|
||||
protected UsernameToUUIDConsumerImpl(String username, ICacheProvider cacheProvider, Consumer<CacheFetchedProfile> responseConsumer) {
|
||||
this.username = username;
|
||||
this.cacheProvider = cacheProvider;
|
||||
this.responseConsumer = responseConsumer;
|
||||
}
|
||||
|
||||
protected void doAccept(CacheFetchedProfile v) {
|
||||
try {
|
||||
responseConsumer.accept(v);
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown caching new profile!", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(Response response) {
|
||||
if(response == null || response.exception != null || response.code != 200 || response.data == null) {
|
||||
doAccept(null);
|
||||
}else {
|
||||
try {
|
||||
JsonObject json = JsonParser.parseString(new String(response.data, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
String loadUsername = json.get("name").getAsString().toLowerCase();
|
||||
if(!username.equals(loadUsername)) {
|
||||
doAccept(null);
|
||||
}
|
||||
UUID mojangUUID = SkinService.parseMojangUUID(json.get("id").getAsString());
|
||||
lookupProfileByUUID(mojangUUID, cacheProvider, responseConsumer, false);
|
||||
}catch(Throwable t) {
|
||||
doAccept(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final SimpleRateLimiter rateLimitDownload = new SimpleRateLimiter();
|
||||
private static final SimpleRateLimiter rateLimitLookup = new SimpleRateLimiter();
|
||||
|
||||
public static void downloadSkin(String skinTexture, ICacheProvider cacheProvider, Consumer<byte[]> responseConsumer) {
|
||||
downloadSkin(SkinPackets.createEaglerURLSkinUUID(skinTexture), skinTexture, cacheProvider, responseConsumer);
|
||||
}
|
||||
|
||||
public static void downloadSkin(UUID skinUUID, String skinTexture, ICacheProvider cacheProvider, Consumer<byte[]> responseConsumer) {
|
||||
CacheLoadedSkin loadedSkin = cacheProvider.loadSkinByUUID(skinUUID);
|
||||
if(loadedSkin == null) {
|
||||
URI uri;
|
||||
try {
|
||||
uri = URI.create(skinTexture);
|
||||
}catch(IllegalArgumentException ex) {
|
||||
try {
|
||||
responseConsumer.accept(null);
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown handling invalid skin!", t);
|
||||
}
|
||||
throw new CancelException();
|
||||
}
|
||||
int globalRatelimit = EaglerXVelocity.getEagler().getConfig().getSkinRateLimitGlobal();
|
||||
boolean isRateLimit;
|
||||
synchronized(rateLimitDownload) {
|
||||
isRateLimit = !rateLimitDownload.rateLimit(globalRatelimit);
|
||||
}
|
||||
if(!isRateLimit) {
|
||||
BinaryHttpClient.asyncRequest("GET", uri, new SkinConsumerImpl(
|
||||
new SkinCachingConsumer(skinUUID, skinTexture, cacheProvider, responseConsumer)));
|
||||
}else {
|
||||
EaglerXVelocity.logger().warn("skin system reached the global texture download ratelimit of {} while downloading up \"{}\"", globalRatelimit, skinTexture);
|
||||
throw new CancelException();
|
||||
}
|
||||
}else {
|
||||
try {
|
||||
responseConsumer.accept(loadedSkin.texture);
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown processing cached skin!", t);
|
||||
}
|
||||
throw new CancelException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void lookupProfileByUUID(UUID playerUUID, ICacheProvider cacheProvider, Consumer<CacheFetchedProfile> responseConsumer) {
|
||||
lookupProfileByUUID(playerUUID, cacheProvider, responseConsumer, true);
|
||||
}
|
||||
|
||||
private static void lookupProfileByUUID(UUID playerUUID, ICacheProvider cacheProvider, Consumer<CacheFetchedProfile> responseConsumer, boolean rateLimit) {
|
||||
CacheLoadedProfile profile = cacheProvider.loadProfileByUUID(playerUUID);
|
||||
if(profile == null) {
|
||||
URI requestURI = URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + SkinService.getMojangUUID(playerUUID));
|
||||
int globalRatelimit = EaglerXVelocity.getEagler().getConfig().getUuidRateLimitGlobal();
|
||||
boolean isRateLimit;
|
||||
if(rateLimit) {
|
||||
synchronized(rateLimitLookup) {
|
||||
isRateLimit = !rateLimitLookup.rateLimit(globalRatelimit);
|
||||
}
|
||||
}else {
|
||||
isRateLimit = false;
|
||||
}
|
||||
if(!isRateLimit) {
|
||||
BinaryHttpClient.asyncRequest("GET", requestURI, new ProfileConsumerImpl(playerUUID,
|
||||
new ProfileCachingConsumer(playerUUID, cacheProvider, responseConsumer)));
|
||||
}else {
|
||||
EaglerXVelocity.logger().warn("skin system reached the global UUID lookup ratelimit of {} while looking up \"{}\"", globalRatelimit, playerUUID);
|
||||
throw new CancelException();
|
||||
}
|
||||
}else {
|
||||
try {
|
||||
responseConsumer.accept(new CacheFetchedProfile(profile));
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown processing cached profile!", t);
|
||||
}
|
||||
throw new CancelException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void lookupProfileByUsername(String playerUsername, ICacheProvider cacheProvider, Consumer<CacheFetchedProfile> responseConsumer) {
|
||||
String playerUsernameLower = playerUsername.toLowerCase();
|
||||
CacheLoadedProfile profile = cacheProvider.loadProfileByUsername(playerUsernameLower);
|
||||
if(profile == null) {
|
||||
if(!playerUsernameLower.equals(playerUsernameLower.replaceAll("[^a-z0-9_]", "_").trim())) {
|
||||
try {
|
||||
responseConsumer.accept(null);
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown processing invalid profile!", t);
|
||||
}
|
||||
throw new CancelException();
|
||||
}
|
||||
URI requestURI = URI.create("https://api.mojang.com/users/profiles/minecraft/" + playerUsername);
|
||||
int globalRatelimit = EaglerXVelocity.getEagler().getConfig().getUuidRateLimitGlobal();
|
||||
boolean isRateLimit;
|
||||
synchronized(rateLimitLookup) {
|
||||
isRateLimit = !rateLimitLookup.rateLimit(globalRatelimit);
|
||||
}
|
||||
if(!isRateLimit) {
|
||||
BinaryHttpClient.asyncRequest("GET", requestURI, new UsernameToUUIDConsumerImpl(playerUsername, cacheProvider, responseConsumer));
|
||||
}else {
|
||||
EaglerXVelocity.logger().warn("skin system reached the global UUID lookup ratelimit of {} while looking up \"{}\"", globalRatelimit, playerUsername);
|
||||
throw new CancelException();
|
||||
}
|
||||
}else {
|
||||
try {
|
||||
responseConsumer.accept(new CacheFetchedProfile(profile));
|
||||
}catch(Throwable t) {
|
||||
EaglerXVelocity.logger().error("Exception thrown processing cached profile!", t);
|
||||
}
|
||||
throw new CancelException();
|
||||
}
|
||||
}
|
||||
|
||||
public static class CancelException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,857 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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
|
||||
*
|
||||
* http://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.
|
||||
*/
|
||||
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Provides Base64 encoding and decoding as defined by
|
||||
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>.
|
||||
*
|
||||
* <p>
|
||||
* This class implements section <cite>6.8. Base64
|
||||
* Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose Internet
|
||||
* Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by
|
||||
* Freed and Borenstein.
|
||||
* </p>
|
||||
* <p>
|
||||
* The class can be parameterized in the following manner with various
|
||||
* constructors:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>URL-safe mode: Default off.</li>
|
||||
* <li>Line length: Default 76. Line length that aren't multiples of 4 will
|
||||
* still essentially end up being multiples of 4 in the encoded data.
|
||||
* <li>Line separator: Default is CRLF ("\r\n")</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The URL-safe parameter is only applied to encode operations. Decoding
|
||||
* seamlessly handles both modes.
|
||||
* </p>
|
||||
* <p>
|
||||
* Since this class operates directly on byte streams, and not character
|
||||
* streams, it is hard-coded to only encode/decode character encodings which are
|
||||
* compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8,
|
||||
* etc).
|
||||
* </p>
|
||||
* <p>
|
||||
* This class is thread-safe.
|
||||
* </p>
|
||||
*
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
|
||||
* @since 1.0
|
||||
*/
|
||||
public class Base64 extends BaseNCodec {
|
||||
|
||||
/**
|
||||
* BASE32 characters are 6 bits in length. They are formed by taking a block of
|
||||
* 3 octets to form a 24-bit string, which is converted into 4 BASE64
|
||||
* characters.
|
||||
*/
|
||||
private static final int BITS_PER_ENCODED_BYTE = 6;
|
||||
private static final int BYTES_PER_UNENCODED_BLOCK = 3;
|
||||
private static final int BYTES_PER_ENCODED_BLOCK = 4;
|
||||
|
||||
/**
|
||||
* This array is a lookup table that translates 6-bit positive integer index
|
||||
* values into their "Base64 Alphabet" equivalents as specified in Table 1 of
|
||||
* RFC 2045.
|
||||
*
|
||||
* Thanks to "commons" project in ws.apache.org for this code.
|
||||
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
|
||||
*/
|
||||
private static final byte[] STANDARD_ENCODE_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
||||
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1',
|
||||
'2', '3', '4', '5', '6', '7', '8', '9', '+', '/' };
|
||||
|
||||
/**
|
||||
* This is a copy of the STANDARD_ENCODE_TABLE above, but with + and / changed
|
||||
* to - and _ to make the encoded Base64 results more URL-SAFE. This table is
|
||||
* only used when the Base64's mode is set to URL-SAFE.
|
||||
*/
|
||||
private static final byte[] URL_SAFE_ENCODE_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
||||
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1',
|
||||
'2', '3', '4', '5', '6', '7', '8', '9', '-', '_' };
|
||||
|
||||
/**
|
||||
* This array is a lookup table that translates Unicode characters drawn from
|
||||
* the "Base64 Alphabet" (as specified in Table 1 of RFC 2045) into their 6-bit
|
||||
* positive integer equivalents. Characters that are not in the Base64 alphabet
|
||||
* but fall within the bounds of the array are translated to -1.
|
||||
*
|
||||
* Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This
|
||||
* means decoder seamlessly handles both URL_SAFE and STANDARD base64. (The
|
||||
* encoder, on the other hand, needs to know ahead of time what to emit).
|
||||
*
|
||||
* Thanks to "commons" project in ws.apache.org for this code.
|
||||
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
|
||||
*/
|
||||
private static final byte[] DECODE_TABLE = {
|
||||
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f
|
||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, // 20-2f + - /
|
||||
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 30-3f 0-9
|
||||
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-O
|
||||
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, // 50-5f P-Z _
|
||||
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o
|
||||
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 // 70-7a p-z
|
||||
};
|
||||
|
||||
/**
|
||||
* Base64 uses 6-bit fields.
|
||||
*/
|
||||
/** Mask used to extract 6 bits, used when encoding */
|
||||
private static final int MASK_6BITS = 0x3f;
|
||||
/** Mask used to extract 4 bits, used when decoding final trailing character. */
|
||||
private static final int MASK_4BITS = 0xf;
|
||||
/** Mask used to extract 2 bits, used when decoding final trailing character. */
|
||||
private static final int MASK_2BITS = 0x3;
|
||||
|
||||
// The static final fields above are used for the original static byte[] methods
|
||||
// on Base64.
|
||||
// The private member fields below are used with the new streaming approach,
|
||||
// which requires
|
||||
// some state be preserved between calls of encode() and decode().
|
||||
|
||||
/**
|
||||
* Decodes Base64 data into octets.
|
||||
* <p>
|
||||
* <b>Note:</b> this method seamlessly handles data encoded in URL-safe or
|
||||
* normal mode.
|
||||
* </p>
|
||||
*
|
||||
* @param base64Data Byte array containing Base64 data
|
||||
* @return Array containing decoded data.
|
||||
*/
|
||||
public static byte[] decodeBase64(final byte[] base64Data) {
|
||||
return new Base64().decode(base64Data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a Base64 String into octets.
|
||||
* <p>
|
||||
* <b>Note:</b> this method seamlessly handles data encoded in URL-safe or
|
||||
* normal mode.
|
||||
* </p>
|
||||
*
|
||||
* @param base64String String containing Base64 data
|
||||
* @return Array containing decoded data.
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] decodeBase64(final String base64String) {
|
||||
return new Base64().decode(base64String);
|
||||
}
|
||||
|
||||
// Implementation of integer encoding used for crypto
|
||||
/**
|
||||
* Decodes a byte64-encoded integer according to crypto standards such as W3C's
|
||||
* XML-Signature.
|
||||
*
|
||||
* @param pArray a byte array containing base64 character data
|
||||
* @return A BigInteger
|
||||
* @since 1.4
|
||||
*/
|
||||
public static BigInteger decodeInteger(final byte[] pArray) {
|
||||
return new BigInteger(1, decodeBase64(pArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes binary data using the base64 algorithm but does not chunk the output.
|
||||
*
|
||||
* @param binaryData binary data to encode
|
||||
* @return byte[] containing Base64 characters in their UTF-8 representation.
|
||||
*/
|
||||
public static byte[] encodeBase64(final byte[] binaryData) {
|
||||
return encodeBase64(binaryData, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes binary data using the base64 algorithm, optionally chunking the
|
||||
* output into 76 character blocks.
|
||||
*
|
||||
* @param binaryData Array containing binary data to encode.
|
||||
* @param isChunked if {@code true} this encoder will chunk the base64 output
|
||||
* into 76 character blocks
|
||||
* @return Base64-encoded data.
|
||||
* @throws IllegalArgumentException Thrown when the input array needs an output
|
||||
* array bigger than {@link Integer#MAX_VALUE}
|
||||
*/
|
||||
public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked) {
|
||||
return encodeBase64(binaryData, isChunked, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes binary data using the base64 algorithm, optionally chunking the
|
||||
* output into 76 character blocks.
|
||||
*
|
||||
* @param binaryData Array containing binary data to encode.
|
||||
* @param isChunked if {@code true} this encoder will chunk the base64 output
|
||||
* into 76 character blocks
|
||||
* @param urlSafe if {@code true} this encoder will emit - and _ instead of
|
||||
* the usual + and / characters. <b>Note: no padding is added
|
||||
* when encoding using the URL-safe alphabet.</b>
|
||||
* @return Base64-encoded data.
|
||||
* @throws IllegalArgumentException Thrown when the input array needs an output
|
||||
* array bigger than {@link Integer#MAX_VALUE}
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe) {
|
||||
return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes binary data using the base64 algorithm, optionally chunking the
|
||||
* output into 76 character blocks.
|
||||
*
|
||||
* @param binaryData Array containing binary data to encode.
|
||||
* @param isChunked if {@code true} this encoder will chunk the base64
|
||||
* output into 76 character blocks
|
||||
* @param urlSafe if {@code true} this encoder will emit - and _ instead
|
||||
* of the usual + and / characters. <b>Note: no padding is
|
||||
* added when encoding using the URL-safe alphabet.</b>
|
||||
* @param maxResultSize The maximum result size to accept.
|
||||
* @return Base64-encoded data.
|
||||
* @throws IllegalArgumentException Thrown when the input array needs an output
|
||||
* array bigger than maxResultSize
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe,
|
||||
final int maxResultSize) {
|
||||
if (binaryData == null || binaryData.length == 0) {
|
||||
return binaryData;
|
||||
}
|
||||
|
||||
// Create this so can use the super-class method
|
||||
// Also ensures that the same roundings are performed by the ctor and the code
|
||||
final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe);
|
||||
final long len = b64.getEncodedLength(binaryData);
|
||||
if (len > maxResultSize) {
|
||||
throw new IllegalArgumentException("Input array too big, the output array would be bigger (" + len
|
||||
+ ") than the specified maximum size of " + maxResultSize);
|
||||
}
|
||||
|
||||
return b64.encode(binaryData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes binary data using the base64 algorithm and chunks the encoded output
|
||||
* into 76 character blocks
|
||||
*
|
||||
* @param binaryData binary data to encode
|
||||
* @return Base64 characters chunked in 76 character blocks
|
||||
*/
|
||||
public static byte[] encodeBase64Chunked(final byte[] binaryData) {
|
||||
return encodeBase64(binaryData, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes binary data using the base64 algorithm but does not chunk the output.
|
||||
*
|
||||
* NOTE: We changed the behavior of this method from multi-line chunking
|
||||
* (commons-codec-1.4) to single-line non-chunking (commons-codec-1.5).
|
||||
*
|
||||
* @param binaryData binary data to encode
|
||||
* @return String containing Base64 characters.
|
||||
* @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not).
|
||||
*/
|
||||
public static String encodeBase64String(final byte[] binaryData) {
|
||||
return new String(encodeBase64(binaryData, false), Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes binary data using a URL-safe variation of the base64 algorithm but
|
||||
* does not chunk the output. The url-safe variation emits - and _ instead of +
|
||||
* and / characters. <b>Note: no padding is added.</b>
|
||||
*
|
||||
* @param binaryData binary data to encode
|
||||
* @return byte[] containing Base64 characters in their UTF-8 representation.
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] encodeBase64URLSafe(final byte[] binaryData) {
|
||||
return encodeBase64(binaryData, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes binary data using a URL-safe variation of the base64 algorithm but
|
||||
* does not chunk the output. The url-safe variation emits - and _ instead of +
|
||||
* and / characters. <b>Note: no padding is added.</b>
|
||||
*
|
||||
* @param binaryData binary data to encode
|
||||
* @return String containing Base64 characters
|
||||
* @since 1.4
|
||||
*/
|
||||
public static String encodeBase64URLSafeString(final byte[] binaryData) {
|
||||
return new String(encodeBase64(binaryData, false, true), Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes to a byte64-encoded integer according to crypto standards such as
|
||||
* W3C's XML-Signature.
|
||||
*
|
||||
* @param bigInteger a BigInteger
|
||||
* @return A byte array containing base64 character data
|
||||
* @throws NullPointerException if null is passed in
|
||||
* @since 1.4
|
||||
*/
|
||||
public static byte[] encodeInteger(final BigInteger bigInteger) {
|
||||
return encodeBase64(toIntegerBytes(bigInteger), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a given byte array to see if it contains only valid characters within
|
||||
* the Base64 alphabet. Currently the method treats whitespace as valid.
|
||||
*
|
||||
* @param arrayOctet byte array to test
|
||||
* @return {@code true} if all bytes are valid characters in the Base64 alphabet
|
||||
* or if the byte array is empty; {@code false}, otherwise
|
||||
* @deprecated 1.5 Use {@link #isBase64(byte[])}, will be removed in 2.0.
|
||||
*/
|
||||
@Deprecated
|
||||
public static boolean isArrayByteBase64(final byte[] arrayOctet) {
|
||||
return isBase64(arrayOctet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the {@code octet} is in the base 64 alphabet.
|
||||
*
|
||||
* @param octet The value to test
|
||||
* @return {@code true} if the value is defined in the the base 64 alphabet,
|
||||
* {@code false} otherwise.
|
||||
* @since 1.4
|
||||
*/
|
||||
public static boolean isBase64(final byte octet) {
|
||||
return octet == PAD_DEFAULT || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a given byte array to see if it contains only valid characters within
|
||||
* the Base64 alphabet. Currently the method treats whitespace as valid.
|
||||
*
|
||||
* @param arrayOctet byte array to test
|
||||
* @return {@code true} if all bytes are valid characters in the Base64 alphabet
|
||||
* or if the byte array is empty; {@code false}, otherwise
|
||||
* @since 1.5
|
||||
*/
|
||||
public static boolean isBase64(final byte[] arrayOctet) {
|
||||
for (int i = 0; i < arrayOctet.length; i++) {
|
||||
if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a given String to see if it contains only valid characters within the
|
||||
* Base64 alphabet. Currently the method treats whitespace as valid.
|
||||
*
|
||||
* @param base64 String to test
|
||||
* @return {@code true} if all characters in the String are valid characters in
|
||||
* the Base64 alphabet or if the String is empty; {@code false},
|
||||
* otherwise
|
||||
* @since 1.5
|
||||
*/
|
||||
public static boolean isBase64(final String base64) {
|
||||
return isBase64(base64.getBytes(Charset.forName("UTF-8")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a byte-array representation of a {@code BigInteger} without sign bit.
|
||||
*
|
||||
* @param bigInt {@code BigInteger} to be converted
|
||||
* @return a byte array representation of the BigInteger parameter
|
||||
*/
|
||||
static byte[] toIntegerBytes(final BigInteger bigInt) {
|
||||
int bitlen = bigInt.bitLength();
|
||||
// round bitlen
|
||||
bitlen = ((bitlen + 7) >> 3) << 3;
|
||||
final byte[] bigBytes = bigInt.toByteArray();
|
||||
|
||||
if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {
|
||||
return bigBytes;
|
||||
}
|
||||
// set up params for copying everything but sign bit
|
||||
int startSrc = 0;
|
||||
int len = bigBytes.length;
|
||||
|
||||
// if bigInt is exactly byte-aligned, just skip signbit in copy
|
||||
if ((bigInt.bitLength() % 8) == 0) {
|
||||
startSrc = 1;
|
||||
len--;
|
||||
}
|
||||
final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
|
||||
final byte[] resizedBytes = new byte[bitlen / 8];
|
||||
System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
|
||||
return resizedBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE
|
||||
* above remains static because it is able to decode both STANDARD and URL_SAFE
|
||||
* streams, but the encodeTable must be a member variable so we can switch
|
||||
* between the two modes.
|
||||
*/
|
||||
private final byte[] encodeTable;
|
||||
|
||||
// Only one decode table currently; keep for consistency with Base32 code
|
||||
private final byte[] decodeTable = DECODE_TABLE;
|
||||
|
||||
/**
|
||||
* Line separator for encoding. Not used when decoding. Only used if lineLength
|
||||
* > 0.
|
||||
*/
|
||||
private final byte[] lineSeparator;
|
||||
|
||||
/**
|
||||
* Convenience variable to help us determine when our buffer is going to run out
|
||||
* of room and needs resizing. {@code decodeSize = 3 + lineSeparator.length;}
|
||||
*/
|
||||
private final int decodeSize;
|
||||
|
||||
/**
|
||||
* Convenience variable to help us determine when our buffer is going to run out
|
||||
* of room and needs resizing. {@code encodeSize = 4 + lineSeparator.length;}
|
||||
*/
|
||||
private final int encodeSize;
|
||||
|
||||
/**
|
||||
* Creates a Base64 codec used for decoding (all modes) and encoding in
|
||||
* URL-unsafe mode.
|
||||
* <p>
|
||||
* When encoding the line length is 0 (no chunking), and the encoding table is
|
||||
* STANDARD_ENCODE_TABLE.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* When decoding all variants are supported.
|
||||
* </p>
|
||||
*/
|
||||
public Base64() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Base64 codec used for decoding (all modes) and encoding in the
|
||||
* given URL-safe mode.
|
||||
* <p>
|
||||
* When encoding the line length is 76, the line separator is CRLF, and the
|
||||
* encoding table is STANDARD_ENCODE_TABLE.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* When decoding all variants are supported.
|
||||
* </p>
|
||||
*
|
||||
* @param urlSafe if {@code true}, URL-safe encoding is used. In most cases this
|
||||
* should be set to {@code false}.
|
||||
* @since 1.4
|
||||
*/
|
||||
public Base64(final boolean urlSafe) {
|
||||
this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Base64 codec used for decoding (all modes) and encoding in
|
||||
* URL-unsafe mode.
|
||||
* <p>
|
||||
* When encoding the line length is given in the constructor, the line separator
|
||||
* is CRLF, and the encoding table is STANDARD_ENCODE_TABLE.
|
||||
* </p>
|
||||
* <p>
|
||||
* Line lengths that aren't multiples of 4 will still essentially end up being
|
||||
* multiples of 4 in the encoded data.
|
||||
* </p>
|
||||
* <p>
|
||||
* When decoding all variants are supported.
|
||||
* </p>
|
||||
*
|
||||
* @param lineLength Each line of encoded data will be at most of the given
|
||||
* length (rounded down to nearest multiple of 4). If
|
||||
* lineLength <= 0, then the output will not be divided
|
||||
* into lines (chunks). Ignored when decoding.
|
||||
* @since 1.4
|
||||
*/
|
||||
public Base64(final int lineLength) {
|
||||
this(lineLength, CHUNK_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Base64 codec used for decoding (all modes) and encoding in
|
||||
* URL-unsafe mode.
|
||||
* <p>
|
||||
* When encoding the line length and line separator are given in the
|
||||
* constructor, and the encoding table is STANDARD_ENCODE_TABLE.
|
||||
* </p>
|
||||
* <p>
|
||||
* Line lengths that aren't multiples of 4 will still essentially end up being
|
||||
* multiples of 4 in the encoded data.
|
||||
* </p>
|
||||
* <p>
|
||||
* When decoding all variants are supported.
|
||||
* </p>
|
||||
*
|
||||
* @param lineLength Each line of encoded data will be at most of the given
|
||||
* length (rounded down to nearest multiple of 4). If
|
||||
* lineLength <= 0, then the output will not be divided
|
||||
* into lines (chunks). Ignored when decoding.
|
||||
* @param lineSeparator Each line of encoded data will end with this sequence of
|
||||
* bytes.
|
||||
* @throws IllegalArgumentException Thrown when the provided lineSeparator
|
||||
* included some base64 characters.
|
||||
* @since 1.4
|
||||
*/
|
||||
public Base64(final int lineLength, final byte[] lineSeparator) {
|
||||
this(lineLength, lineSeparator, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Base64 codec used for decoding (all modes) and encoding in
|
||||
* URL-unsafe mode.
|
||||
* <p>
|
||||
* When encoding the line length and line separator are given in the
|
||||
* constructor, and the encoding table is STANDARD_ENCODE_TABLE.
|
||||
* </p>
|
||||
* <p>
|
||||
* Line lengths that aren't multiples of 4 will still essentially end up being
|
||||
* multiples of 4 in the encoded data.
|
||||
* </p>
|
||||
* <p>
|
||||
* When decoding all variants are supported.
|
||||
* </p>
|
||||
*
|
||||
* @param lineLength Each line of encoded data will be at most of the given
|
||||
* length (rounded down to nearest multiple of 4). If
|
||||
* lineLength <= 0, then the output will not be divided
|
||||
* into lines (chunks). Ignored when decoding.
|
||||
* @param lineSeparator Each line of encoded data will end with this sequence of
|
||||
* bytes.
|
||||
* @param urlSafe Instead of emitting '+' and '/' we emit '-' and '_'
|
||||
* respectively. urlSafe is only applied to encode
|
||||
* operations. Decoding seamlessly handles both modes.
|
||||
* <b>Note: no padding is added when using the URL-safe
|
||||
* alphabet.</b>
|
||||
* @throws IllegalArgumentException Thrown when the {@code lineSeparator}
|
||||
* contains Base64 characters.
|
||||
* @since 1.4
|
||||
*/
|
||||
public Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) {
|
||||
this(lineLength, lineSeparator, urlSafe, CodecPolicy.LENIANT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Base64 codec used for decoding (all modes) and encoding in
|
||||
* URL-unsafe mode.
|
||||
* <p>
|
||||
* When encoding the line length and line separator are given in the
|
||||
* constructor, and the encoding table is STANDARD_ENCODE_TABLE.
|
||||
* </p>
|
||||
* <p>
|
||||
* Line lengths that aren't multiples of 4 will still essentially end up being
|
||||
* multiples of 4 in the encoded data.
|
||||
* </p>
|
||||
* <p>
|
||||
* When decoding all variants are supported.
|
||||
* </p>
|
||||
*
|
||||
* @param lineLength Each line of encoded data will be at most of the given
|
||||
* length (rounded down to nearest multiple of 4). If
|
||||
* lineLength <= 0, then the output will not be divided
|
||||
* into lines (chunks). Ignored when decoding.
|
||||
* @param lineSeparator Each line of encoded data will end with this sequence
|
||||
* of bytes.
|
||||
* @param urlSafe Instead of emitting '+' and '/' we emit '-' and '_'
|
||||
* respectively. urlSafe is only applied to encode
|
||||
* operations. Decoding seamlessly handles both modes.
|
||||
* <b>Note: no padding is added when using the URL-safe
|
||||
* alphabet.</b>
|
||||
* @param decodingPolicy The decoding policy.
|
||||
* @throws IllegalArgumentException Thrown when the {@code lineSeparator}
|
||||
* contains Base64 characters.
|
||||
* @since 1.15
|
||||
*/
|
||||
public Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe,
|
||||
final CodecPolicy decodingPolicy) {
|
||||
super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, lineLength,
|
||||
lineSeparator == null ? 0 : lineSeparator.length, PAD_DEFAULT, decodingPolicy);
|
||||
// TODO could be simplified if there is no requirement to reject invalid line
|
||||
// sep when length <=0
|
||||
// @see test case Base64Test.testConstructors()
|
||||
if (lineSeparator != null) {
|
||||
if (containsAlphabetOrPad(lineSeparator)) {
|
||||
final String sep = new String(lineSeparator, Charset.forName("UTF-8"));
|
||||
throw new IllegalArgumentException("lineSeparator must not contain base64 characters: [" + sep + "]");
|
||||
}
|
||||
if (lineLength > 0) { // null line-sep forces no chunking rather than throwing IAE
|
||||
this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length;
|
||||
this.lineSeparator = new byte[lineSeparator.length];
|
||||
System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);
|
||||
} else {
|
||||
this.encodeSize = BYTES_PER_ENCODED_BLOCK;
|
||||
this.lineSeparator = null;
|
||||
}
|
||||
} else {
|
||||
this.encodeSize = BYTES_PER_ENCODED_BLOCK;
|
||||
this.lineSeparator = null;
|
||||
}
|
||||
this.decodeSize = this.encodeSize - 1;
|
||||
this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE;
|
||||
}
|
||||
|
||||
// Implementation of the Encoder Interface
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Decodes all of the provided data, starting at inPos, for inAvail bytes.
|
||||
* Should be called at least twice: once with the data to decode, and once with
|
||||
* inAvail set to "-1" to alert decoder that EOF has been reached. The "-1" call
|
||||
* is not necessary when decoding, but it doesn't hurt, either.
|
||||
* </p>
|
||||
* <p>
|
||||
* Ignores all non-base64 characters. This is how chunked (e.g. 76 character)
|
||||
* data is handled, since CR and LF are silently ignored, but has implications
|
||||
* for other bytes, too. This method subscribes to the garbage-in, garbage-out
|
||||
* philosophy: it will not check the provided data for validity.
|
||||
* </p>
|
||||
* <p>
|
||||
* Thanks to "commons" project in ws.apache.org for the bitwise operations, and
|
||||
* general approach.
|
||||
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
|
||||
* </p>
|
||||
*
|
||||
* @param in byte[] array of ascii data to base64 decode.
|
||||
* @param inPos Position to start reading data from.
|
||||
* @param inAvail Amount of bytes available from input for decoding.
|
||||
* @param context the context to be used
|
||||
*/
|
||||
@Override
|
||||
void decode(final byte[] in, int inPos, final int inAvail, final Context context) {
|
||||
if (context.eof) {
|
||||
return;
|
||||
}
|
||||
if (inAvail < 0) {
|
||||
context.eof = true;
|
||||
}
|
||||
for (int i = 0; i < inAvail; i++) {
|
||||
final byte[] buffer = ensureBufferSize(decodeSize, context);
|
||||
final byte b = in[inPos++];
|
||||
if (b == pad) {
|
||||
// We're done.
|
||||
context.eof = true;
|
||||
break;
|
||||
}
|
||||
if (b >= 0 && b < DECODE_TABLE.length) {
|
||||
final int result = DECODE_TABLE[b];
|
||||
if (result >= 0) {
|
||||
context.modulus = (context.modulus + 1) % BYTES_PER_ENCODED_BLOCK;
|
||||
context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result;
|
||||
if (context.modulus == 0) {
|
||||
buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS);
|
||||
buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
|
||||
buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two forms of EOF as far as base64 decoder is concerned: actual
|
||||
// EOF (-1) and first time '=' character is encountered in stream.
|
||||
// This approach makes the '=' padding characters completely optional.
|
||||
if (context.eof && context.modulus != 0) {
|
||||
final byte[] buffer = ensureBufferSize(decodeSize, context);
|
||||
|
||||
// We have some spare bits remaining
|
||||
// Output all whole multiples of 8 bits and ignore the rest
|
||||
switch (context.modulus) {
|
||||
// case 0 : // impossible, as excluded above
|
||||
case 1: // 6 bits - either ignore entirely, or raise an exception
|
||||
validateTrailingCharacter();
|
||||
break;
|
||||
case 2: // 12 bits = 8 + 4
|
||||
validateCharacter(MASK_4BITS, context);
|
||||
context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits
|
||||
buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
|
||||
break;
|
||||
case 3: // 18 bits = 8 + 8 + 2
|
||||
validateCharacter(MASK_2BITS, context);
|
||||
context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits
|
||||
buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
|
||||
buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Impossible modulus " + context.modulus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Encodes all of the provided data, starting at inPos, for inAvail bytes. Must
|
||||
* be called at least twice: once with the data to encode, and once with inAvail
|
||||
* set to "-1" to alert encoder that EOF has been reached, to flush last
|
||||
* remaining bytes (if not multiple of 3).
|
||||
* </p>
|
||||
* <p>
|
||||
* <b>Note: no padding is added when encoding using the URL-safe alphabet.</b>
|
||||
* </p>
|
||||
* <p>
|
||||
* Thanks to "commons" project in ws.apache.org for the bitwise operations, and
|
||||
* general approach.
|
||||
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
|
||||
* </p>
|
||||
*
|
||||
* @param in byte[] array of binary data to base64 encode.
|
||||
* @param inPos Position to start reading data from.
|
||||
* @param inAvail Amount of bytes available from input for encoding.
|
||||
* @param context the context to be used
|
||||
*/
|
||||
@Override
|
||||
void encode(final byte[] in, int inPos, final int inAvail, final Context context) {
|
||||
if (context.eof) {
|
||||
return;
|
||||
}
|
||||
// inAvail < 0 is how we're informed of EOF in the underlying data we're
|
||||
// encoding.
|
||||
if (inAvail < 0) {
|
||||
context.eof = true;
|
||||
if (0 == context.modulus && lineLength == 0) {
|
||||
return; // no leftovers to process and not using chunking
|
||||
}
|
||||
final byte[] buffer = ensureBufferSize(encodeSize, context);
|
||||
final int savedPos = context.pos;
|
||||
switch (context.modulus) { // 0-2
|
||||
case 0: // nothing to do here
|
||||
break;
|
||||
case 1: // 8 bits = 6 + 2
|
||||
// top 6 bits:
|
||||
buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2) & MASK_6BITS];
|
||||
// remaining 2:
|
||||
buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4) & MASK_6BITS];
|
||||
// URL-SAFE skips the padding to further reduce size.
|
||||
if (encodeTable == STANDARD_ENCODE_TABLE) {
|
||||
buffer[context.pos++] = pad;
|
||||
buffer[context.pos++] = pad;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // 16 bits = 6 + 6 + 4
|
||||
buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10) & MASK_6BITS];
|
||||
buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4) & MASK_6BITS];
|
||||
buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2) & MASK_6BITS];
|
||||
// URL-SAFE skips the padding to further reduce size.
|
||||
if (encodeTable == STANDARD_ENCODE_TABLE) {
|
||||
buffer[context.pos++] = pad;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Impossible modulus " + context.modulus);
|
||||
}
|
||||
context.currentLinePos += context.pos - savedPos; // keep track of current line position
|
||||
// if currentPos == 0 we are at the start of a line, so don't add CRLF
|
||||
if (lineLength > 0 && context.currentLinePos > 0) {
|
||||
System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);
|
||||
context.pos += lineSeparator.length;
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < inAvail; i++) {
|
||||
final byte[] buffer = ensureBufferSize(encodeSize, context);
|
||||
context.modulus = (context.modulus + 1) % BYTES_PER_UNENCODED_BLOCK;
|
||||
int b = in[inPos++];
|
||||
if (b < 0) {
|
||||
b += 256;
|
||||
}
|
||||
context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE
|
||||
if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract
|
||||
buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18) & MASK_6BITS];
|
||||
buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12) & MASK_6BITS];
|
||||
buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6) & MASK_6BITS];
|
||||
buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS];
|
||||
context.currentLinePos += BYTES_PER_ENCODED_BLOCK;
|
||||
if (lineLength > 0 && lineLength <= context.currentLinePos) {
|
||||
System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);
|
||||
context.pos += lineSeparator.length;
|
||||
context.currentLinePos = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the {@code octet} is in the Base64 alphabet.
|
||||
*
|
||||
* @param octet The value to test
|
||||
* @return {@code true} if the value is defined in the the Base64 alphabet
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
@Override
|
||||
protected boolean isInAlphabet(final byte octet) {
|
||||
return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns our current encode mode. True if we're URL-SAFE, false otherwise.
|
||||
*
|
||||
* @return true if we're in URL-SAFE mode, false otherwise.
|
||||
* @since 1.4
|
||||
*/
|
||||
public boolean isUrlSafe() {
|
||||
return this.encodeTable == URL_SAFE_ENCODE_TABLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether decoding the final trailing character is possible in the
|
||||
* context of the set of possible base 64 values.
|
||||
*
|
||||
* <p>
|
||||
* The character is valid if the lower bits within the provided mask are zero.
|
||||
* This is used to test the final trailing base-64 digit is zero in the bits
|
||||
* that will be discarded.
|
||||
*
|
||||
* @param emptyBitsMask The mask of the lower bits that should be empty
|
||||
* @param context the context to be used
|
||||
*
|
||||
* @throws IllegalArgumentException if the bits being checked contain any
|
||||
* non-zero value
|
||||
*/
|
||||
private void validateCharacter(final int emptyBitsMask, final Context context) {
|
||||
if (isStrictDecoding() && (context.ibitWorkArea & emptyBitsMask) != 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Strict decoding: Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible encoding. "
|
||||
+ "Expected the discarded bits from the character to be zero.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates whether decoding allows an entire final trailing character that
|
||||
* cannot be used for a complete byte.
|
||||
*
|
||||
* @throws IllegalArgumentException if strict decoding is enabled
|
||||
*/
|
||||
private void validateTrailingCharacter() {
|
||||
if (isStrictDecoding()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Strict decoding: Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible encoding. "
|
||||
+ "Decoding requires at least two trailing 6-bit characters to create bytes.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,694 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
|
||||
public abstract class BaseNCodec {
|
||||
|
||||
static enum CodecPolicy {
|
||||
STRICT, LENIANT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds thread context so classes can be thread-safe.
|
||||
*
|
||||
* This class is not itself thread-safe; each thread must allocate its own copy.
|
||||
*
|
||||
* @since 1.7
|
||||
*/
|
||||
static class Context {
|
||||
|
||||
/**
|
||||
* Place holder for the bytes we're dealing with for our based logic. Bitwise
|
||||
* operations store and extract the encoding or decoding from this variable.
|
||||
*/
|
||||
int ibitWorkArea;
|
||||
|
||||
/**
|
||||
* Place holder for the bytes we're dealing with for our based logic. Bitwise
|
||||
* operations store and extract the encoding or decoding from this variable.
|
||||
*/
|
||||
long lbitWorkArea;
|
||||
|
||||
/**
|
||||
* Buffer for streaming.
|
||||
*/
|
||||
byte[] buffer;
|
||||
|
||||
/**
|
||||
* Position where next character should be written in the buffer.
|
||||
*/
|
||||
int pos;
|
||||
|
||||
/**
|
||||
* Position where next character should be read from the buffer.
|
||||
*/
|
||||
int readPos;
|
||||
|
||||
/**
|
||||
* Boolean flag to indicate the EOF has been reached. Once EOF has been reached,
|
||||
* this object becomes useless, and must be thrown away.
|
||||
*/
|
||||
boolean eof;
|
||||
|
||||
/**
|
||||
* Variable tracks how many characters have been written to the current line.
|
||||
* Only used when encoding. We use it to make sure each encoded line never goes
|
||||
* beyond lineLength (if lineLength > 0).
|
||||
*/
|
||||
int currentLinePos;
|
||||
|
||||
/**
|
||||
* Writes to the buffer only occur after every 3/5 reads when encoding, and
|
||||
* every 4/8 reads when decoding. This variable helps track that.
|
||||
*/
|
||||
int modulus;
|
||||
|
||||
Context() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a String useful for debugging (especially within a debugger.)
|
||||
*
|
||||
* @return a String useful for debugging.
|
||||
*/
|
||||
@SuppressWarnings("boxing") // OK to ignore boxing here
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"%s[buffer=%s, currentLinePos=%s, eof=%s, ibitWorkArea=%s, lbitWorkArea=%s, "
|
||||
+ "modulus=%s, pos=%s, readPos=%s]",
|
||||
this.getClass().getSimpleName(), Arrays.toString(buffer), currentLinePos, eof, ibitWorkArea,
|
||||
lbitWorkArea, modulus, pos, readPos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EOF
|
||||
*
|
||||
* @since 1.7
|
||||
*/
|
||||
static final int EOF = -1;
|
||||
|
||||
/**
|
||||
* MIME chunk size per RFC 2045 section 6.8.
|
||||
*
|
||||
* <p>
|
||||
* The {@value} character limit does not count the trailing CRLF, but counts all
|
||||
* other characters, including any equal signs.
|
||||
* </p>
|
||||
*
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
|
||||
*/
|
||||
public static final int MIME_CHUNK_SIZE = 76;
|
||||
|
||||
/**
|
||||
* PEM chunk size per RFC 1421 section 4.3.2.4.
|
||||
*
|
||||
* <p>
|
||||
* The {@value} character limit does not count the trailing CRLF, but counts all
|
||||
* other characters, including any equal signs.
|
||||
* </p>
|
||||
*
|
||||
* @see <a href="http://tools.ietf.org/html/rfc1421">RFC 1421 section
|
||||
* 4.3.2.4</a>
|
||||
*/
|
||||
public static final int PEM_CHUNK_SIZE = 64;
|
||||
|
||||
private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2;
|
||||
|
||||
/**
|
||||
* Defines the default buffer size - currently {@value} - must be large enough
|
||||
* for at least one encoded block+separator
|
||||
*/
|
||||
private static final int DEFAULT_BUFFER_SIZE = 8192;
|
||||
|
||||
/**
|
||||
* The maximum size buffer to allocate.
|
||||
*
|
||||
* <p>
|
||||
* This is set to the same size used in the JDK {@code java.util.ArrayList}:
|
||||
* </p>
|
||||
* <blockquote> Some VMs reserve some header words in an array. Attempts to
|
||||
* allocate larger arrays may result in OutOfMemoryError: Requested array size
|
||||
* exceeds VM limit. </blockquote>
|
||||
*/
|
||||
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
|
||||
|
||||
/** Mask used to extract 8 bits, used in decoding bytes */
|
||||
protected static final int MASK_8BITS = 0xff;
|
||||
|
||||
/**
|
||||
* Byte used to pad output.
|
||||
*/
|
||||
protected static final byte PAD_DEFAULT = '='; // Allow static access to default
|
||||
|
||||
/**
|
||||
* Chunk separator per RFC 2045 section 2.1.
|
||||
*
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
|
||||
*/
|
||||
static final byte[] CHUNK_SEPARATOR = { '\r', '\n' };
|
||||
|
||||
/**
|
||||
* Compares two {@code int} values numerically treating the values as unsigned.
|
||||
* Taken from JDK 1.8.
|
||||
*
|
||||
* <p>
|
||||
* TODO: Replace with JDK 1.8 Integer::compareUnsigned(int, int).
|
||||
* </p>
|
||||
*
|
||||
* @param x the first {@code int} to compare
|
||||
* @param y the second {@code int} to compare
|
||||
* @return the value {@code 0} if {@code x == y}; a value less than {@code 0} if
|
||||
* {@code x < y} as unsigned values; and a value greater than {@code 0}
|
||||
* if {@code x > y} as unsigned values
|
||||
*/
|
||||
private static int compareUnsigned(final int xx, final int yy) {
|
||||
int x = xx + Integer.MIN_VALUE;
|
||||
int y = yy + Integer.MIN_VALUE;
|
||||
return (x < y) ? -1 : ((x == y) ? 0 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a positive capacity at least as large the minimum required capacity.
|
||||
* If the minimum capacity is negative then this throws an OutOfMemoryError as
|
||||
* no array can be allocated.
|
||||
*
|
||||
* @param minCapacity the minimum capacity
|
||||
* @return the capacity
|
||||
* @throws OutOfMemoryError if the {@code minCapacity} is negative
|
||||
*/
|
||||
private static int createPositiveCapacity(final int minCapacity) {
|
||||
if (minCapacity < 0) {
|
||||
// overflow
|
||||
throw new OutOfMemoryError("Unable to allocate array size: " + (minCapacity & 0xffffffffL));
|
||||
}
|
||||
// This is called when we require buffer expansion to a very big array.
|
||||
// Use the conservative maximum buffer size if possible, otherwise the biggest
|
||||
// required.
|
||||
//
|
||||
// Note: In this situation JDK 1.8 java.util.ArrayList returns
|
||||
// Integer.MAX_VALUE.
|
||||
// This excludes some VMs that can exceed MAX_BUFFER_SIZE but not allocate a
|
||||
// full
|
||||
// Integer.MAX_VALUE length array.
|
||||
// The result is that we may have to allocate an array of this size more than
|
||||
// once if
|
||||
// the capacity must be expanded again.
|
||||
return (minCapacity > MAX_BUFFER_SIZE) ? minCapacity : MAX_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a copy of the chunk separator per RFC 2045 section 2.1.
|
||||
*
|
||||
* @return the chunk separator
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
|
||||
* @since 1.15
|
||||
*/
|
||||
public static byte[] getChunkSeparator() {
|
||||
return CHUNK_SEPARATOR.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a byte value is whitespace or not. Whitespace is taken to mean:
|
||||
* space, tab, CR, LF
|
||||
*
|
||||
* @param byteToCheck the byte to check
|
||||
* @return true if byte is whitespace, false otherwise
|
||||
*/
|
||||
protected static boolean isWhiteSpace(final byte byteToCheck) {
|
||||
switch (byteToCheck) {
|
||||
case ' ':
|
||||
case '\n':
|
||||
case '\r':
|
||||
case '\t':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}.
|
||||
*
|
||||
* @param context the context to be used
|
||||
* @param minCapacity the minimum required capacity
|
||||
* @return the resized byte[] buffer
|
||||
* @throws OutOfMemoryError if the {@code minCapacity} is negative
|
||||
*/
|
||||
private static byte[] resizeBuffer(final Context context, final int minCapacity) {
|
||||
// Overflow-conscious code treats the min and new capacity as unsigned.
|
||||
final int oldCapacity = context.buffer.length;
|
||||
int newCapacity = oldCapacity * DEFAULT_BUFFER_RESIZE_FACTOR;
|
||||
if (compareUnsigned(newCapacity, minCapacity) < 0) {
|
||||
newCapacity = minCapacity;
|
||||
}
|
||||
if (compareUnsigned(newCapacity, MAX_BUFFER_SIZE) > 0) {
|
||||
newCapacity = createPositiveCapacity(minCapacity);
|
||||
}
|
||||
|
||||
final byte[] b = new byte[newCapacity];
|
||||
System.arraycopy(context.buffer, 0, b, 0, context.buffer.length);
|
||||
context.buffer = b;
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #pad}. Will be removed in 2.0.
|
||||
*/
|
||||
@Deprecated
|
||||
protected final byte PAD = PAD_DEFAULT; // instance variable just in case it needs to vary later
|
||||
|
||||
protected final byte pad; // instance variable just in case it needs to vary later
|
||||
|
||||
/**
|
||||
* Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5
|
||||
* for Base32
|
||||
*/
|
||||
private final int unencodedBlockSize;
|
||||
|
||||
/**
|
||||
* Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8
|
||||
* for Base32
|
||||
*/
|
||||
private final int encodedBlockSize;
|
||||
|
||||
/**
|
||||
* Chunksize for encoding. Not used when decoding. A value of zero or less
|
||||
* implies no chunking of the encoded data. Rounded down to nearest multiple of
|
||||
* encodedBlockSize.
|
||||
*/
|
||||
protected final int lineLength;
|
||||
|
||||
/**
|
||||
* Size of chunk separator. Not used unless {@link #lineLength} > 0.
|
||||
*/
|
||||
private final int chunkSeparatorLength;
|
||||
|
||||
/**
|
||||
* Defines the decoding behavior when the input bytes contain leftover trailing
|
||||
* bits that cannot be created by a valid encoding. These can be bits that are
|
||||
* unused from the final character or entire characters. The default mode is
|
||||
* lenient decoding. Set this to {@code true} to enable strict decoding.
|
||||
* <ul>
|
||||
* <li>Lenient: Any trailing bits are composed into 8-bit bytes where possible.
|
||||
* The remainder are discarded.
|
||||
* <li>Strict: The decoding will raise an {@link IllegalArgumentException} if
|
||||
* trailing bits are not part of a valid encoding. Any unused bits from the
|
||||
* final character must be zero. Impossible counts of entire final characters
|
||||
* are not allowed.
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* When strict decoding is enabled it is expected that the decoded bytes will be
|
||||
* re-encoded to a byte array that matches the original, i.e. no changes occur
|
||||
* on the final character. This requires that the input bytes use the same
|
||||
* padding and alphabet as the encoder.
|
||||
*/
|
||||
private final CodecPolicy decodingPolicy;
|
||||
|
||||
/**
|
||||
* Note {@code lineLength} is rounded down to the nearest multiple of the
|
||||
* encoded block size. If {@code chunkSeparatorLength} is zero, then chunking is
|
||||
* disabled.
|
||||
*
|
||||
* @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)
|
||||
* @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)
|
||||
* @param lineLength if > 0, use chunking with a length
|
||||
* {@code lineLength}
|
||||
* @param chunkSeparatorLength the chunk separator length, if relevant
|
||||
*/
|
||||
protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, final int lineLength,
|
||||
final int chunkSeparatorLength) {
|
||||
this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, PAD_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note {@code lineLength} is rounded down to the nearest multiple of the
|
||||
* encoded block size. If {@code chunkSeparatorLength} is zero, then chunking is
|
||||
* disabled.
|
||||
*
|
||||
* @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)
|
||||
* @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)
|
||||
* @param lineLength if > 0, use chunking with a length
|
||||
* {@code lineLength}
|
||||
* @param chunkSeparatorLength the chunk separator length, if relevant
|
||||
* @param pad byte used as padding byte.
|
||||
*/
|
||||
protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, final int lineLength,
|
||||
final int chunkSeparatorLength, final byte pad) {
|
||||
this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, pad, CodecPolicy.LENIANT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note {@code lineLength} is rounded down to the nearest multiple of the
|
||||
* encoded block size. If {@code chunkSeparatorLength} is zero, then chunking is
|
||||
* disabled.
|
||||
*
|
||||
* @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3)
|
||||
* @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4)
|
||||
* @param lineLength if > 0, use chunking with a length
|
||||
* {@code lineLength}
|
||||
* @param chunkSeparatorLength the chunk separator length, if relevant
|
||||
* @param pad byte used as padding byte.
|
||||
* @param decodingPolicy Decoding policy.
|
||||
* @since 1.15
|
||||
*/
|
||||
protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, final int lineLength,
|
||||
final int chunkSeparatorLength, final byte pad, final CodecPolicy decodingPolicy) {
|
||||
this.unencodedBlockSize = unencodedBlockSize;
|
||||
this.encodedBlockSize = encodedBlockSize;
|
||||
final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0;
|
||||
this.lineLength = useChunking ? (lineLength / encodedBlockSize) * encodedBlockSize : 0;
|
||||
this.chunkSeparatorLength = chunkSeparatorLength;
|
||||
this.pad = pad;
|
||||
this.decodingPolicy = decodingPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of buffered data available for reading.
|
||||
*
|
||||
* @param context the context to be used
|
||||
* @return The amount of buffered data available for reading.
|
||||
*/
|
||||
int available(final Context context) { // package protected for access from I/O streams
|
||||
return context.buffer != null ? context.pos - context.readPos : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a given byte array to see if it contains any characters within the
|
||||
* alphabet or PAD.
|
||||
*
|
||||
* Intended for use in checking line-ending arrays
|
||||
*
|
||||
* @param arrayOctet byte array to test
|
||||
* @return {@code true} if any byte is a valid character in the alphabet or PAD;
|
||||
* {@code false} otherwise
|
||||
*/
|
||||
protected boolean containsAlphabetOrPad(final byte[] arrayOctet) {
|
||||
if (arrayOctet == null) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < arrayOctet.length; ++i) {
|
||||
byte element = arrayOctet[i];
|
||||
if (pad == element || isInAlphabet(element)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a byte[] containing characters in the Base-N alphabet.
|
||||
*
|
||||
* @param pArray A byte array containing Base-N character data
|
||||
* @return a byte array containing binary data
|
||||
*/
|
||||
public byte[] decode(final byte[] pArray) {
|
||||
if (pArray == null || pArray.length == 0) {
|
||||
return pArray;
|
||||
}
|
||||
final Context context = new Context();
|
||||
decode(pArray, 0, pArray.length, context);
|
||||
decode(pArray, 0, EOF, context); // Notify decoder of EOF.
|
||||
final byte[] result = new byte[context.pos];
|
||||
readResults(result, 0, result.length, context);
|
||||
return result;
|
||||
}
|
||||
|
||||
// package protected for access from I/O streams
|
||||
abstract void decode(byte[] pArray, int i, int length, Context context);
|
||||
|
||||
/**
|
||||
* Decodes an Object using the Base-N algorithm. This method is provided in
|
||||
* order to satisfy the requirements of the Decoder interface, and will throw a
|
||||
* DecoderException if the supplied object is not of type byte[] or String.
|
||||
*
|
||||
* @param obj Object to decode
|
||||
* @return An object (of type byte[]) containing the binary data which
|
||||
* corresponds to the byte[] or String supplied.
|
||||
* @throws DecoderException if the parameter supplied is not of type byte[]
|
||||
*/
|
||||
public Object decode(final Object obj) {
|
||||
if (obj instanceof byte[]) {
|
||||
return decode((byte[]) obj);
|
||||
} else if (obj instanceof String) {
|
||||
return decode((String) obj);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a String containing characters in the Base-N alphabet.
|
||||
*
|
||||
* @param pArray A String containing Base-N character data
|
||||
* @return a byte array containing binary data
|
||||
*/
|
||||
public byte[] decode(final String pArray) {
|
||||
return decode(pArray.getBytes(Charset.forName("UTF-8")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte[] containing binary data, into a byte[] containing characters
|
||||
* in the alphabet.
|
||||
*
|
||||
* @param pArray a byte array containing binary data
|
||||
* @return A byte array containing only the base N alphabetic character data
|
||||
*/
|
||||
public byte[] encode(final byte[] pArray) {
|
||||
if (pArray == null || pArray.length == 0) {
|
||||
return pArray;
|
||||
}
|
||||
return encode(pArray, 0, pArray.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte[] containing binary data, into a byte[] containing characters
|
||||
* in the alphabet.
|
||||
*
|
||||
* @param pArray a byte array containing binary data
|
||||
* @param offset initial offset of the subarray.
|
||||
* @param length length of the subarray.
|
||||
* @return A byte array containing only the base N alphabetic character data
|
||||
* @since 1.11
|
||||
*/
|
||||
public byte[] encode(final byte[] pArray, final int offset, final int length) {
|
||||
if (pArray == null || pArray.length == 0) {
|
||||
return pArray;
|
||||
}
|
||||
final Context context = new Context();
|
||||
encode(pArray, offset, length, context);
|
||||
encode(pArray, offset, EOF, context); // Notify encoder of EOF.
|
||||
final byte[] buf = new byte[context.pos - context.readPos];
|
||||
readResults(buf, 0, buf.length, context);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// package protected for access from I/O streams
|
||||
abstract void encode(byte[] pArray, int i, int length, Context context);
|
||||
|
||||
/**
|
||||
* Encodes an Object using the Base-N algorithm. This method is provided in
|
||||
* order to satisfy the requirements of the Encoder interface, and will throw an
|
||||
* EncoderException if the supplied object is not of type byte[].
|
||||
*
|
||||
* @param obj Object to encode
|
||||
* @return An object (of type byte[]) containing the Base-N encoded data which
|
||||
* corresponds to the byte[] supplied.
|
||||
* @throws EncoderException if the parameter supplied is not of type byte[]
|
||||
*/
|
||||
public Object encode(final Object obj) {
|
||||
return encode((byte[]) obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte[] containing binary data, into a String containing characters
|
||||
* in the appropriate alphabet. Uses UTF8 encoding.
|
||||
*
|
||||
* @param pArray a byte array containing binary data
|
||||
* @return String containing only character data in the appropriate alphabet.
|
||||
* @since 1.5 This is a duplicate of {@link #encodeToString(byte[])}; it was
|
||||
* merged during refactoring.
|
||||
*/
|
||||
public String encodeAsString(final byte[] pArray) {
|
||||
return new String(encode(pArray), Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a byte[] containing binary data, into a String containing characters
|
||||
* in the Base-N alphabet. Uses UTF8 encoding.
|
||||
*
|
||||
* @param pArray a byte array containing binary data
|
||||
* @return A String containing only Base-N character data
|
||||
*/
|
||||
public String encodeToString(final byte[] pArray) {
|
||||
return new String(encode(pArray), Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the buffer has room for {@code size} bytes
|
||||
*
|
||||
* @param size minimum spare space required
|
||||
* @param context the context to be used
|
||||
* @return the buffer
|
||||
*/
|
||||
protected byte[] ensureBufferSize(final int size, final Context context) {
|
||||
if (context.buffer == null) {
|
||||
context.buffer = new byte[Math.max(size, getDefaultBufferSize())];
|
||||
context.pos = 0;
|
||||
context.readPos = 0;
|
||||
|
||||
// Overflow-conscious:
|
||||
// x + y > z == x + y - z > 0
|
||||
} else if (context.pos + size - context.buffer.length > 0) {
|
||||
return resizeBuffer(context, context.pos + size);
|
||||
}
|
||||
return context.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decoding behavior policy.
|
||||
*
|
||||
* <p>
|
||||
* The default is lenient. If the decoding policy is strict, then decoding will
|
||||
* raise an {@link IllegalArgumentException} if trailing bits are not part of a
|
||||
* valid encoding. Decoding will compose trailing bits into 8-bit bytes and
|
||||
* discard the remainder.
|
||||
* </p>
|
||||
*
|
||||
* @return true if using strict decoding
|
||||
* @since 1.15
|
||||
*/
|
||||
public CodecPolicy getCodecPolicy() {
|
||||
return decodingPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default buffer size. Can be overridden.
|
||||
*
|
||||
* @return the default buffer size.
|
||||
*/
|
||||
protected int getDefaultBufferSize() {
|
||||
return DEFAULT_BUFFER_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the amount of space needed to encode the supplied array.
|
||||
*
|
||||
* @param pArray byte[] array which will later be encoded
|
||||
*
|
||||
* @return amount of space needed to encoded the supplied array. Returns a long
|
||||
* since a max-len array will require > Integer.MAX_VALUE
|
||||
*/
|
||||
public long getEncodedLength(final byte[] pArray) {
|
||||
// Calculate non-chunked size - rounded up to allow for padding
|
||||
// cast to long is needed to avoid possibility of overflow
|
||||
long len = ((pArray.length + unencodedBlockSize - 1) / unencodedBlockSize) * (long) encodedBlockSize;
|
||||
if (lineLength > 0) { // We're using chunking
|
||||
// Round up to nearest multiple
|
||||
len += ((len + lineLength - 1) / lineLength) * chunkSeparatorLength;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this object has buffered data for reading.
|
||||
*
|
||||
* @param context the context to be used
|
||||
* @return true if there is data still available for reading.
|
||||
*/
|
||||
boolean hasData(final Context context) { // package protected for access from I/O streams
|
||||
return context.buffer != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the {@code octet} is in the current alphabet. Does not
|
||||
* allow whitespace or pad.
|
||||
*
|
||||
* @param value The value to test
|
||||
*
|
||||
* @return {@code true} if the value is defined in the current alphabet,
|
||||
* {@code false} otherwise.
|
||||
*/
|
||||
protected abstract boolean isInAlphabet(byte value);
|
||||
|
||||
/**
|
||||
* Tests a given byte array to see if it contains only valid characters within
|
||||
* the alphabet. The method optionally treats whitespace and pad as valid.
|
||||
*
|
||||
* @param arrayOctet byte array to test
|
||||
* @param allowWSPad if {@code true}, then whitespace and PAD are also allowed
|
||||
*
|
||||
* @return {@code true} if all bytes are valid characters in the alphabet or if
|
||||
* the byte array is empty; {@code false}, otherwise
|
||||
*/
|
||||
public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) {
|
||||
for (int i = 0; i < arrayOctet.length; ++i) {
|
||||
byte octet = arrayOctet[i];
|
||||
if (!isInAlphabet(octet) && (!allowWSPad || (octet != pad) && !isWhiteSpace(octet))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a given String to see if it contains only valid characters within the
|
||||
* alphabet. The method treats whitespace and PAD as valid.
|
||||
*
|
||||
* @param basen String to test
|
||||
* @return {@code true} if all characters in the String are valid characters in
|
||||
* the alphabet or if the String is empty; {@code false}, otherwise
|
||||
* @see #isInAlphabet(byte[], boolean)
|
||||
*/
|
||||
public boolean isInAlphabet(final String basen) {
|
||||
return isInAlphabet(basen.getBytes(Charset.forName("UTF-8")), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if decoding behavior is strict. Decoding will raise an
|
||||
* {@link IllegalArgumentException} if trailing bits are not part of a valid
|
||||
* encoding.
|
||||
*
|
||||
* <p>
|
||||
* The default is false for lenient decoding. Decoding will compose trailing
|
||||
* bits into 8-bit bytes and discard the remainder.
|
||||
* </p>
|
||||
*
|
||||
* @return true if using strict decoding
|
||||
* @since 1.15
|
||||
*/
|
||||
public boolean isStrictDecoding() {
|
||||
return decodingPolicy == CodecPolicy.STRICT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts buffered data into the provided byte[] array, starting at position
|
||||
* bPos, up to a maximum of bAvail bytes. Returns how many bytes were actually
|
||||
* extracted.
|
||||
* <p>
|
||||
* Package protected for access from I/O streams.
|
||||
*
|
||||
* @param b byte[] array to extract the buffered data into.
|
||||
* @param bPos position in byte[] array to start extraction at.
|
||||
* @param bAvail amount of bytes we're allowed to extract. We may extract fewer
|
||||
* (if fewer are available).
|
||||
* @param context the context to be used
|
||||
* @return The number of bytes successfully extracted into the provided byte[]
|
||||
* array.
|
||||
*/
|
||||
int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) {
|
||||
if (context.buffer != null) {
|
||||
final int len = Math.min(available(context), bAvail);
|
||||
System.arraycopy(context.buffer, context.readPos, b, bPos, len);
|
||||
context.readPos += len;
|
||||
if (context.readPos >= context.pos) {
|
||||
context.buffer = null; // so hasData() will return false, and this method can return -1
|
||||
}
|
||||
return len;
|
||||
}
|
||||
return context.eof ? EOF : 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,259 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.net.ssl.SSLEngine;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.velocitypowered.proxy.network.TransportType.Type;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.codec.http.DefaultHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpObject;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocityVersion;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 BinaryHttpClient {
|
||||
|
||||
public static class Response {
|
||||
|
||||
public final int code;
|
||||
public final byte[] data;
|
||||
public final Throwable exception;
|
||||
|
||||
public Response(int code, byte[] data) {
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
public Response(Throwable exception) {
|
||||
this.code = -1;
|
||||
this.data = null;
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class NettyHttpChannelFutureListener implements ChannelFutureListener {
|
||||
|
||||
protected final String method;
|
||||
protected final URI requestURI;
|
||||
protected final Consumer<Response> responseCallback;
|
||||
|
||||
protected NettyHttpChannelFutureListener(String method, URI requestURI, Consumer<Response> responseCallback) {
|
||||
this.method = method;
|
||||
this.requestURI = requestURI;
|
||||
this.responseCallback = responseCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
if (future.isSuccess()) {
|
||||
String path = requestURI.getRawPath()
|
||||
+ ((requestURI.getRawQuery() == null) ? "" : ("?" + requestURI.getRawQuery()));
|
||||
HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1,
|
||||
HttpMethod.valueOf(method), path);
|
||||
request.headers().set(HttpHeaderNames.HOST, (Object) requestURI.getHost());
|
||||
request.headers().set(HttpHeaderNames.USER_AGENT, "Mozilla/5.0 " + EaglerXVelocityVersion.ID + "/" + EaglerXVelocityVersion.VERSION);
|
||||
future.channel().writeAndFlush(request);
|
||||
} else {
|
||||
addressCache.invalidate(requestURI.getHost());
|
||||
responseCallback.accept(new Response(new IOException("Connection failed")));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class NettyHttpChannelInitializer extends ChannelInitializer<Channel> {
|
||||
|
||||
protected final Consumer<Response> responseCallback;
|
||||
protected final boolean ssl;
|
||||
protected final String host;
|
||||
protected final int port;
|
||||
|
||||
protected NettyHttpChannelInitializer(Consumer<Response> responseCallback, boolean ssl, String host, int port) {
|
||||
this.responseCallback = responseCallback;
|
||||
this.ssl = ssl;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initChannel(Channel ch) throws Exception {
|
||||
ch.pipeline().addLast("timeout", new ReadTimeoutHandler(5L, TimeUnit.SECONDS));
|
||||
if (this.ssl) {
|
||||
SSLEngine engine = SslContextBuilder.forClient().build().newEngine(ch.alloc(), host, port);
|
||||
ch.pipeline().addLast("ssl", new SslHandler(engine));
|
||||
}
|
||||
|
||||
ch.pipeline().addLast("http", new HttpClientCodec());
|
||||
ch.pipeline().addLast("handler", new NettyHttpResponseHandler(responseCallback));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class NettyHttpResponseHandler extends SimpleChannelInboundHandler<HttpObject> {
|
||||
|
||||
protected final Consumer<Response> responseCallback;
|
||||
protected int responseCode = -1;
|
||||
protected ByteBuf buffer = null;
|
||||
|
||||
protected NettyHttpResponseHandler(Consumer<Response> responseCallback) {
|
||||
this.responseCallback = responseCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
|
||||
if (msg instanceof HttpResponse) {
|
||||
HttpResponse response = (HttpResponse) msg;
|
||||
responseCode = response.status().code();
|
||||
if (responseCode == HttpResponseStatus.NO_CONTENT.code()) {
|
||||
this.done(ctx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (msg instanceof HttpContent) {
|
||||
HttpContent content = (HttpContent) msg;
|
||||
if(buffer == null) {
|
||||
buffer = ctx.alloc().buffer();
|
||||
}
|
||||
this.buffer.writeBytes(content.content());
|
||||
if (msg instanceof LastHttpContent) {
|
||||
this.done(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
responseCallback.accept(new Response(cause));
|
||||
}
|
||||
|
||||
private void done(ChannelHandlerContext ctx) {
|
||||
try {
|
||||
byte[] array;
|
||||
if(buffer != null) {
|
||||
array = new byte[buffer.readableBytes()];
|
||||
buffer.readBytes(array);
|
||||
buffer.release();
|
||||
}else {
|
||||
array = new byte[0];
|
||||
}
|
||||
responseCallback.accept(new Response(responseCode, array));
|
||||
}finally {
|
||||
ctx.channel().pipeline().remove(this);
|
||||
ctx.channel().close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final Cache<String, InetAddress> addressCache = CacheBuilder.newBuilder().expireAfterWrite(15L, TimeUnit.MINUTES).build();
|
||||
private static EventLoopGroup eventLoop = null;
|
||||
|
||||
public static void asyncRequest(String method, URI uri, Consumer<Response> responseCallback) {
|
||||
EventLoopGroup eventLoop = getEventLoopGroup();
|
||||
|
||||
int port = uri.getPort();
|
||||
boolean ssl = false;
|
||||
String scheme = uri.getScheme();
|
||||
switch(scheme) {
|
||||
case "http":
|
||||
if(port == -1) {
|
||||
port = 80;
|
||||
}
|
||||
break;
|
||||
case "https":
|
||||
if(port == -1) {
|
||||
port = 443;
|
||||
}
|
||||
ssl = true;
|
||||
break;
|
||||
default:
|
||||
responseCallback.accept(new Response(new UnsupportedOperationException("Unsupported scheme: " + scheme)));
|
||||
return;
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
InetAddress inetHost = addressCache.getIfPresent(host);
|
||||
if (inetHost == null) {
|
||||
try {
|
||||
inetHost = InetAddress.getByName(host);
|
||||
} catch (UnknownHostException ex) {
|
||||
responseCallback.accept(new Response(ex));
|
||||
return;
|
||||
}
|
||||
addressCache.put(host, inetHost);
|
||||
}
|
||||
|
||||
(new Bootstrap()).channelFactory(EaglerXVelocity.getEagler().getChannelFactory()).group(eventLoop)
|
||||
.handler(new NettyHttpChannelInitializer(responseCallback, ssl, host, port))
|
||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000).option(ChannelOption.TCP_NODELAY, true)
|
||||
.remoteAddress(inetHost, port).connect()
|
||||
.addListener(new NettyHttpChannelFutureListener(method, uri, responseCallback));
|
||||
}
|
||||
|
||||
private static EventLoopGroup getEventLoopGroup() {
|
||||
if(eventLoop == null) {
|
||||
eventLoop = EaglerXVelocity.getEagler().getTransportType().createEventLoopGroup(Type.WORKER);
|
||||
}
|
||||
return eventLoop;
|
||||
}
|
||||
|
||||
public static void killEventLoop() {
|
||||
if(eventLoop != null) {
|
||||
EaglerXVelocity.logger().info("Stopping skin cache HTTP client...");
|
||||
eventLoop.shutdownGracefully();
|
||||
try {
|
||||
eventLoop.awaitTermination(30l, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException var13) {
|
||||
;
|
||||
}
|
||||
eventLoop = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 CapePackets {
|
||||
|
||||
public static final int PACKET_MY_CAPE_PRESET = 0x01;
|
||||
public static final int PACKET_MY_CAPE_CUSTOM = 0x02;
|
||||
public static final int PACKET_GET_OTHER_CAPE = 0x03;
|
||||
public static final int PACKET_OTHER_CAPE_PRESET = 0x04;
|
||||
public static final int PACKET_OTHER_CAPE_CUSTOM = 0x05;
|
||||
|
||||
public static void processPacket(byte[] data, ConnectedPlayer sender, CapeServiceOffline capeService) throws IOException {
|
||||
if(data.length == 0) {
|
||||
throw new IOException("Zero-length packet recieved");
|
||||
}
|
||||
int packetId = (int)data[0] & 0xFF;
|
||||
try {
|
||||
switch(packetId) {
|
||||
case PACKET_GET_OTHER_CAPE:
|
||||
processGetOtherCape(data, sender, capeService);
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unknown packet type " + packetId);
|
||||
}
|
||||
}catch(IOException ex) {
|
||||
throw ex;
|
||||
}catch(Throwable t) {
|
||||
throw new IOException("Unhandled exception handling cape packet type " + packetId, t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void processGetOtherCape(byte[] data, ConnectedPlayer sender, CapeServiceOffline capeService) throws IOException {
|
||||
if(data.length != 17) {
|
||||
throw new IOException("Invalid length " + data.length + " for skin request packet");
|
||||
}
|
||||
UUID searchUUID = SkinPackets.bytesToUUID(data, 1);
|
||||
capeService.processGetOtherCape(searchUUID, sender);
|
||||
}
|
||||
|
||||
public static void registerEaglerPlayer(UUID clientUUID, byte[] bs, CapeServiceOffline capeService) throws IOException {
|
||||
if(bs.length == 0) {
|
||||
throw new IOException("Zero-length packet recieved");
|
||||
}
|
||||
byte[] generatedPacket;
|
||||
int packetType = (int)bs[0] & 0xFF;
|
||||
switch(packetType) {
|
||||
case PACKET_MY_CAPE_PRESET:
|
||||
if(bs.length != 5) {
|
||||
throw new IOException("Invalid length " + bs.length + " for preset cape packet");
|
||||
}
|
||||
generatedPacket = CapePackets.makePresetResponse(clientUUID, (bs[1] << 24) | (bs[2] << 16) | (bs[3] << 8) | (bs[4] & 0xFF));
|
||||
break;
|
||||
case PACKET_MY_CAPE_CUSTOM:
|
||||
if(bs.length != 1174) {
|
||||
throw new IOException("Invalid length " + bs.length + " for custom cape packet");
|
||||
}
|
||||
generatedPacket = CapePackets.makeCustomResponse(clientUUID, bs, 1, 1173);
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unknown skin packet type: " + packetType);
|
||||
}
|
||||
capeService.registerEaglercraftPlayer(clientUUID, generatedPacket);
|
||||
}
|
||||
|
||||
public static void registerEaglerPlayerFallback(UUID clientUUID, CapeServiceOffline capeService) {
|
||||
capeService.registerEaglercraftPlayer(clientUUID, CapePackets.makePresetResponse(clientUUID, 0));
|
||||
}
|
||||
|
||||
public static byte[] makePresetResponse(UUID uuid, int presetId) {
|
||||
byte[] ret = new byte[1 + 16 + 4];
|
||||
ret[0] = (byte)PACKET_OTHER_CAPE_PRESET;
|
||||
SkinPackets.UUIDToBytes(uuid, ret, 1);
|
||||
ret[17] = (byte)(presetId >> 24);
|
||||
ret[18] = (byte)(presetId >> 16);
|
||||
ret[19] = (byte)(presetId >> 8);
|
||||
ret[20] = (byte)(presetId & 0xFF);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static byte[] makeCustomResponse(UUID uuid, byte[] pixels) {
|
||||
return makeCustomResponse(uuid, pixels, 0, pixels.length);
|
||||
}
|
||||
|
||||
public static byte[] makeCustomResponse(UUID uuid, byte[] pixels, int offset, int length) {
|
||||
byte[] ret = new byte[1 + 16 + length];
|
||||
ret[0] = (byte)PACKET_OTHER_CAPE_CUSTOM;
|
||||
SkinPackets.UUIDToBytes(uuid, ret, 1);
|
||||
System.arraycopy(pixels, offset, ret, 17, length);
|
||||
return ret;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 CapeServiceOffline {
|
||||
|
||||
public static final int masterRateLimitPerPlayer = 250;
|
||||
|
||||
public static final ChannelIdentifier CHANNEL = new LegacyChannelIdentifier("EAG|Capes-1.8");
|
||||
|
||||
private final Map<UUID, byte[]> capesCache = new HashMap();
|
||||
|
||||
public void registerEaglercraftPlayer(UUID playerUUID, byte[] capePacket) {
|
||||
synchronized(capesCache) {
|
||||
capesCache.put(playerUUID, capePacket);
|
||||
}
|
||||
}
|
||||
|
||||
public void processGetOtherCape(UUID searchUUID, ConnectedPlayer sender) {
|
||||
if(EaglerPipeline.getEaglerHandle(sender).skinLookupRateLimiter.rateLimit(masterRateLimitPerPlayer)) {
|
||||
byte[] maybeCape;
|
||||
synchronized(capesCache) {
|
||||
maybeCape = capesCache.get(searchUUID);
|
||||
}
|
||||
if(maybeCape != null) {
|
||||
sender.sendPluginMessage(CapeServiceOffline.CHANNEL, maybeCape);
|
||||
}else {
|
||||
sender.sendPluginMessage(CapeServiceOffline.CHANNEL, CapePackets.makePresetResponse(searchUUID, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterPlayer(UUID playerUUID) {
|
||||
synchronized(capesCache) {
|
||||
capesCache.remove(playerUUID);
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
synchronized(capesCache) {
|
||||
capesCache.clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 interface ICacheProvider {
|
||||
|
||||
public static class CacheException extends RuntimeException {
|
||||
|
||||
public CacheException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public CacheException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public CacheException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CacheException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CacheLoadedSkin {
|
||||
|
||||
public final UUID uuid;
|
||||
public final String url;
|
||||
public final byte[] texture;
|
||||
|
||||
public CacheLoadedSkin(UUID uuid, String url, byte[] texture) {
|
||||
this.uuid = uuid;
|
||||
this.url = url;
|
||||
this.texture = texture;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class CacheLoadedProfile {
|
||||
|
||||
public final UUID uuid;
|
||||
public final String username;
|
||||
public final String texture;
|
||||
public final String model;
|
||||
|
||||
public CacheLoadedProfile(UUID uuid, String username, String texture, String model) {
|
||||
this.uuid = uuid;
|
||||
this.username = username;
|
||||
this.texture = texture;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public UUID getSkinUUID() {
|
||||
return SkinPackets.createEaglerURLSkinUUID(texture);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CacheLoadedSkin loadSkinByUUID(UUID uuid) throws CacheException;
|
||||
|
||||
void cacheSkinByUUID(UUID uuid, String url, byte[] textureBlob) throws CacheException;
|
||||
|
||||
CacheLoadedProfile loadProfileByUUID(UUID uuid) throws CacheException;
|
||||
|
||||
CacheLoadedProfile loadProfileByUsername(String username) throws CacheException;
|
||||
|
||||
void cacheProfileByUUID(UUID uuid, String username, String texture, String model) throws CacheException;
|
||||
|
||||
void flush() throws CacheException;
|
||||
|
||||
void destroy();
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 interface ISkinService {
|
||||
|
||||
void init(String uri, String driverClass, String driverPath, int keepObjectsDays, int keepProfilesDays,
|
||||
int maxObjects, int maxProfiles);
|
||||
|
||||
void processGetOtherSkin(final UUID searchUUID, final ConnectedPlayer sender);
|
||||
|
||||
void processGetOtherSkin(UUID searchUUID, String skinURL, ConnectedPlayer sender);
|
||||
|
||||
void registerEaglercraftPlayer(UUID clientUUID, byte[] generatedPacket, int modelId);
|
||||
|
||||
void unregisterPlayer(UUID clientUUID);
|
||||
|
||||
default void registerTextureToPlayerAssociation(String textureURL, UUID playerUUID) {
|
||||
registerTextureToPlayerAssociation(SkinPackets.createEaglerURLSkinUUID(textureURL), playerUUID);
|
||||
}
|
||||
|
||||
void registerTextureToPlayerAssociation(UUID textureUUID, UUID playerUUID);
|
||||
|
||||
void flush();
|
||||
|
||||
void shutdown();
|
||||
|
||||
}
|
@ -0,0 +1,405 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Date;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.sqlite.EaglerDrivers;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 JDBCCacheProvider implements ICacheProvider {
|
||||
|
||||
public static JDBCCacheProvider initialize(String uri, String driverClass, String driverPath, int keepObjectsDays,
|
||||
int keepProfilesDays, int maxObjects, int maxProfiles) throws CacheException {
|
||||
Connection conn;
|
||||
try {
|
||||
conn = EaglerDrivers.connectToDatabase(uri, driverClass, driverPath, new Properties());
|
||||
if(conn == null) {
|
||||
throw new IllegalStateException("Connection is null");
|
||||
}
|
||||
}catch(Throwable t) {
|
||||
throw new CacheException("Could not initialize '" + uri + "'!", t);
|
||||
}
|
||||
EaglerXVelocity.logger().info("Connected to database: " + uri);
|
||||
try {
|
||||
try(Statement stmt = conn.createStatement()) {
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "\"eaglercraft_skins_objects\" ("
|
||||
+ "\"TextureUUID\" TEXT(32) NOT NULL,"
|
||||
+ "\"TextureURL\" VARCHAR(256) NOT NULL,"
|
||||
+ "\"TextureTime\" DATETIME NOT NULL,"
|
||||
+ "\"TextureData\" BLOB,"
|
||||
+ "\"TextureLength\" INT(24) NOT NULL,"
|
||||
+ "PRIMARY KEY(\"TextureUUID\"))");
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS "
|
||||
+ "\"eaglercraft_skins_profiles\" ("
|
||||
+ "\"ProfileUUID\" TEXT(32) NOT NULL,"
|
||||
+ "\"ProfileName\" TEXT(16) NOT NULL,"
|
||||
+ "\"ProfileTime\" DATETIME NOT NULL,"
|
||||
+ "\"ProfileTexture\" VARCHAR(256),"
|
||||
+ "\"ProfileModel\" VARCHAR(16) NOT NULL,"
|
||||
+ "PRIMARY KEY(\"ProfileUUID\"))");
|
||||
stmt.execute("CREATE INDEX IF NOT EXISTS \"profile_name_index\" "
|
||||
+ "ON \"eaglercraft_skins_profiles\" (\"ProfileName\")");
|
||||
}
|
||||
JDBCCacheProvider cacheProvider = new JDBCCacheProvider(conn, uri, keepObjectsDays, keepProfilesDays, maxObjects, maxProfiles);
|
||||
cacheProvider.flush();
|
||||
return cacheProvider;
|
||||
}catch(CacheException ex) {
|
||||
try {
|
||||
conn.close();
|
||||
}catch(SQLException exx) {
|
||||
}
|
||||
throw ex;
|
||||
}catch(Throwable t) {
|
||||
try {
|
||||
conn.close();
|
||||
}catch(SQLException exx) {
|
||||
}
|
||||
throw new CacheException("Could not initialize '" + uri + "'!", t);
|
||||
}
|
||||
}
|
||||
|
||||
protected final Connection connection;
|
||||
protected final String uri;
|
||||
|
||||
protected final PreparedStatement discardExpiredObjects;
|
||||
protected final PreparedStatement discardExpiredProfiles;
|
||||
protected final PreparedStatement getTotalObjects;
|
||||
protected final PreparedStatement getTotalProfiles;
|
||||
protected final PreparedStatement deleteSomeOldestObjects;
|
||||
protected final PreparedStatement deleteSomeOldestProfiles;
|
||||
protected final PreparedStatement querySkinByUUID;
|
||||
protected final PreparedStatement queryProfileByUUID;
|
||||
protected final PreparedStatement queryProfileByUsername;
|
||||
protected final PreparedStatement cacheNewSkin;
|
||||
protected final PreparedStatement cacheNewProfile;
|
||||
protected final PreparedStatement cacheHasSkin;
|
||||
protected final PreparedStatement cacheHasProfile;
|
||||
protected final PreparedStatement cacheUpdateSkin;
|
||||
protected final PreparedStatement cacheUpdateProfile;
|
||||
|
||||
protected long lastFlush;
|
||||
|
||||
protected int keepObjectsDays;
|
||||
protected int keepProfilesDays;
|
||||
protected int maxObjects;
|
||||
protected int maxProfiles;
|
||||
|
||||
protected JDBCCacheProvider(Connection conn, String uri, int keepObjectsDays, int keepProfilesDays, int maxObjects,
|
||||
int maxProfiles) throws SQLException {
|
||||
this.connection = conn;
|
||||
this.uri = uri;
|
||||
this.lastFlush = 0l;
|
||||
this.keepObjectsDays = keepObjectsDays;
|
||||
this.keepProfilesDays = keepProfilesDays;
|
||||
this.maxObjects = maxObjects;
|
||||
this.maxProfiles = maxProfiles;
|
||||
|
||||
this.discardExpiredObjects = connection.prepareStatement("DELETE FROM eaglercraft_skins_objects WHERE textureTime < ?");
|
||||
this.discardExpiredProfiles = connection.prepareStatement("DELETE FROM eaglercraft_skins_profiles WHERE profileTime < ?");
|
||||
this.getTotalObjects = connection.prepareStatement("SELECT COUNT(*) AS total_objects FROM eaglercraft_skins_objects");
|
||||
this.getTotalProfiles = connection.prepareStatement("SELECT COUNT(*) AS total_profiles FROM eaglercraft_skins_profiles");
|
||||
this.deleteSomeOldestObjects = connection.prepareStatement("DELETE FROM eaglercraft_skins_objects WHERE TextureUUID IN (SELECT TextureUUID FROM eaglercraft_skins_objects ORDER BY TextureTime ASC LIMIT ?)");
|
||||
this.deleteSomeOldestProfiles = connection.prepareStatement("DELETE FROM eaglercraft_skins_profiles WHERE ProfileUUID IN (SELECT ProfileUUID FROM eaglercraft_skins_profiles ORDER BY ProfileTime ASC LIMIT ?)");
|
||||
this.querySkinByUUID = connection.prepareStatement("SELECT TextureURL,TextureData,TextureLength FROM eaglercraft_skins_objects WHERE TextureUUID = ? LIMIT 1");
|
||||
this.queryProfileByUUID = connection.prepareStatement("SELECT ProfileName,ProfileTexture,ProfileModel FROM eaglercraft_skins_profiles WHERE ProfileUUID = ? LIMIT 1");
|
||||
this.queryProfileByUsername = connection.prepareStatement("SELECT ProfileUUID,ProfileTexture,ProfileModel FROM eaglercraft_skins_profiles WHERE ProfileName = ? LIMIT 1");
|
||||
this.cacheNewSkin = connection.prepareStatement("INSERT INTO eaglercraft_skins_objects (TextureUUID, TextureURL, TextureTime, TextureData, TextureLength) VALUES(?, ?, ?, ?, ?)");
|
||||
this.cacheNewProfile = connection.prepareStatement("INSERT INTO eaglercraft_skins_profiles (ProfileUUID, ProfileName, ProfileTime, ProfileTexture, ProfileModel) VALUES(?, ?, ?, ?, ?)");
|
||||
this.cacheHasSkin = connection.prepareStatement("SELECT COUNT(TextureUUID) AS has_object FROM eaglercraft_skins_objects WHERE TextureUUID = ? LIMIT 1");
|
||||
this.cacheHasProfile = connection.prepareStatement("SELECT COUNT(ProfileUUID) AS has_profile FROM eaglercraft_skins_profiles WHERE ProfileUUID = ? LIMIT 1");
|
||||
this.cacheUpdateSkin = connection.prepareStatement("UPDATE eaglercraft_skins_objects SET TextureURL = ?, TextureTime = ?, TextureData = ?, TextureLength = ? WHERE TextureUUID = ?");
|
||||
this.cacheUpdateProfile = connection.prepareStatement("UPDATE eaglercraft_skins_profiles SET ProfileName = ?, ProfileTime = ?, ProfileTexture = ?, ProfileModel = ? WHERE ProfileUUID = ?");
|
||||
}
|
||||
|
||||
public CacheLoadedSkin loadSkinByUUID(UUID uuid) throws CacheException {
|
||||
String uuidString = SkinService.getMojangUUID(uuid);
|
||||
String queriedUrls;
|
||||
byte[] queriedTexture;
|
||||
int queriedLength;
|
||||
try {
|
||||
synchronized(querySkinByUUID) {
|
||||
querySkinByUUID.setString(1, uuidString);
|
||||
try(ResultSet resultSet = querySkinByUUID.executeQuery()) {
|
||||
if(resultSet.next()) {
|
||||
queriedUrls = resultSet.getString(1);
|
||||
queriedTexture = resultSet.getBytes(2);
|
||||
queriedLength = resultSet.getInt(3);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
throw new CacheException("SQL query failure while loading cached skin", ex);
|
||||
}
|
||||
if(queriedLength == 0) {
|
||||
return new CacheLoadedSkin(uuid, queriedUrls, new byte[0]);
|
||||
}else {
|
||||
byte[] decompressed = new byte[queriedLength];
|
||||
try {
|
||||
GZIPInputStream is = new GZIPInputStream(new ByteArrayInputStream(queriedTexture));
|
||||
int i = 0, j = 0;
|
||||
while(j < queriedLength && (i = is.read(decompressed, j, queriedLength - j)) != -1) {
|
||||
j += i;
|
||||
}
|
||||
}catch(IOException ex) {
|
||||
throw new CacheException("SQL query failure while loading cached skin");
|
||||
}
|
||||
return new CacheLoadedSkin(uuid, queriedUrls, decompressed);
|
||||
}
|
||||
}
|
||||
|
||||
public void cacheSkinByUUID(UUID uuid, String url, byte[] textureBlob) throws CacheException {
|
||||
ByteArrayOutputStream bao = new ByteArrayOutputStream();
|
||||
try {
|
||||
GZIPOutputStream deflateOut = new GZIPOutputStream(bao);
|
||||
deflateOut.write(textureBlob);
|
||||
deflateOut.close();
|
||||
}catch(IOException ex) {
|
||||
throw new CacheException("Skin compression error", ex);
|
||||
}
|
||||
int len;
|
||||
byte[] textureBlobCompressed;
|
||||
if(textureBlob == null || textureBlob.length == 0) {
|
||||
len = 0;
|
||||
textureBlobCompressed = null;
|
||||
}else {
|
||||
len = textureBlob.length;
|
||||
textureBlobCompressed = bao.toByteArray();
|
||||
}
|
||||
try {
|
||||
String uuidString = SkinService.getMojangUUID(uuid);
|
||||
synchronized(cacheNewSkin) {
|
||||
boolean has;
|
||||
cacheHasSkin.setString(1, uuidString);
|
||||
try(ResultSet resultSet = cacheHasSkin.executeQuery()) {
|
||||
if(resultSet.next()) {
|
||||
has = resultSet.getInt(1) > 0;
|
||||
}else {
|
||||
has = false; // ??
|
||||
}
|
||||
}
|
||||
if(has) {
|
||||
cacheUpdateSkin.setString(1, url);
|
||||
cacheUpdateSkin.setDate(2, new Date(System.currentTimeMillis()));
|
||||
cacheUpdateSkin.setBytes(3, textureBlobCompressed);
|
||||
cacheUpdateSkin.setInt(4, len);
|
||||
cacheUpdateSkin.setString(5, uuidString);
|
||||
cacheUpdateSkin.executeUpdate();
|
||||
}else {
|
||||
cacheNewSkin.setString(1, uuidString);
|
||||
cacheNewSkin.setString(2, url);
|
||||
cacheNewSkin.setDate(3, new Date(System.currentTimeMillis()));
|
||||
cacheNewSkin.setBytes(4, textureBlobCompressed);
|
||||
cacheNewSkin.setInt(5, len);
|
||||
cacheNewSkin.executeUpdate();
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
throw new CacheException("SQL query failure while caching new skin", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public CacheLoadedProfile loadProfileByUUID(UUID uuid) throws CacheException {
|
||||
try {
|
||||
String uuidString = SkinService.getMojangUUID(uuid);
|
||||
synchronized(queryProfileByUUID) {
|
||||
queryProfileByUUID.setString(1, uuidString);
|
||||
try(ResultSet resultSet = queryProfileByUUID.executeQuery()) {
|
||||
if(resultSet.next()) {
|
||||
String profileName = resultSet.getString(1);
|
||||
String profileTexture = resultSet.getString(2);
|
||||
String profileModel = resultSet.getString(3);
|
||||
return new CacheLoadedProfile(uuid, profileName, profileTexture, profileModel);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
throw new CacheException("SQL query failure while loading profile by uuid", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public CacheLoadedProfile loadProfileByUsername(String username) throws CacheException {
|
||||
try {
|
||||
synchronized(queryProfileByUsername) {
|
||||
queryProfileByUsername.setString(1, username);
|
||||
try(ResultSet resultSet = queryProfileByUsername.executeQuery()) {
|
||||
if(resultSet.next()) {
|
||||
UUID profileUUID = SkinService.parseMojangUUID(resultSet.getString(1));
|
||||
String profileTexture = resultSet.getString(2);
|
||||
String profileModel = resultSet.getString(3);
|
||||
return new CacheLoadedProfile(profileUUID, username, profileTexture, profileModel);
|
||||
}else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
throw new CacheException("SQL query failure while loading profile by username", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void cacheProfileByUUID(UUID uuid, String username, String texture, String model) throws CacheException {
|
||||
try {
|
||||
String uuidString = SkinService.getMojangUUID(uuid);
|
||||
synchronized(cacheNewProfile) {
|
||||
boolean has;
|
||||
cacheHasProfile.setString(1, uuidString);
|
||||
try(ResultSet resultSet = cacheHasProfile.executeQuery()) {
|
||||
if(resultSet.next()) {
|
||||
has = resultSet.getInt(1) > 0;
|
||||
}else {
|
||||
has = false; // ??
|
||||
}
|
||||
}
|
||||
if(has) {
|
||||
cacheUpdateProfile.setString(1, username);
|
||||
cacheUpdateProfile.setDate(2, new Date(System.currentTimeMillis()));
|
||||
cacheUpdateProfile.setString(3, texture);
|
||||
cacheUpdateProfile.setString(4, model);
|
||||
cacheUpdateProfile.setString(5, uuidString);
|
||||
cacheUpdateProfile.executeUpdate();
|
||||
}else {
|
||||
cacheNewProfile.setString(1, uuidString);
|
||||
cacheNewProfile.setString(2, username);
|
||||
cacheNewProfile.setDate(3, new Date(System.currentTimeMillis()));
|
||||
cacheNewProfile.setString(4, texture);
|
||||
cacheNewProfile.setString(5, model);
|
||||
cacheNewProfile.executeUpdate();
|
||||
}
|
||||
}
|
||||
}catch(SQLException ex) {
|
||||
throw new CacheException("SQL query failure while caching new profile", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
long millis = System.currentTimeMillis();
|
||||
if(millis - lastFlush > 1200000l) { // 30 minutes
|
||||
lastFlush = millis;
|
||||
try {
|
||||
Date expiryObjects = new Date(millis - keepObjectsDays * 86400000l);
|
||||
Date expiryProfiles = new Date(millis - keepProfilesDays * 86400000l);
|
||||
|
||||
synchronized(discardExpiredObjects) {
|
||||
discardExpiredObjects.setDate(1, expiryObjects);
|
||||
discardExpiredObjects.execute();
|
||||
}
|
||||
synchronized(discardExpiredProfiles) {
|
||||
discardExpiredProfiles.setDate(1, expiryProfiles);
|
||||
discardExpiredProfiles.execute();
|
||||
}
|
||||
|
||||
int totalObjects, totalProfiles;
|
||||
|
||||
synchronized(getTotalObjects) {
|
||||
try(ResultSet resultSet = getTotalObjects.executeQuery()) {
|
||||
if(resultSet.next()) {
|
||||
totalObjects = resultSet.getInt(1);
|
||||
}else {
|
||||
throw new SQLException("Empty ResultSet recieved when checking \"eaglercraft_skins_objects\" row count");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(getTotalProfiles) {
|
||||
try(ResultSet resultSet = getTotalProfiles.executeQuery()) {
|
||||
if(resultSet.next()) {
|
||||
totalProfiles = resultSet.getInt(1);
|
||||
}else {
|
||||
throw new SQLException("Empty ResultSet recieved when checking \"eaglercraft_skins_profiles\" row count");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(totalObjects > maxObjects) {
|
||||
int deleteCount = totalObjects - maxObjects + (maxObjects >> 3);
|
||||
EaglerXVelocity.logger().warn(
|
||||
"Skin object cache has passed {} skins in size ({}), deleting {} skins from the cache to free space",
|
||||
maxObjects, totalObjects, deleteCount);
|
||||
synchronized(deleteSomeOldestObjects) {
|
||||
deleteSomeOldestObjects.setInt(1, deleteCount);
|
||||
deleteSomeOldestObjects.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
if(totalProfiles > maxProfiles) {
|
||||
int deleteCount = totalProfiles - maxProfiles + (maxProfiles >> 3);
|
||||
EaglerXVelocity.logger().warn(
|
||||
"Skin profile cache has passed {} profiles in size ({}), deleting {} profiles from the cache to free space",
|
||||
maxProfiles, totalProfiles, deleteCount);
|
||||
synchronized(deleteSomeOldestProfiles) {
|
||||
deleteSomeOldestProfiles.setInt(1, deleteCount);
|
||||
deleteSomeOldestProfiles.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
}catch(SQLException ex) {
|
||||
throw new CacheException("SQL query failure while flushing cache!", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void destroyStatement(Statement stmt) {
|
||||
try {
|
||||
stmt.close();
|
||||
} catch (SQLException e) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
destroyStatement(discardExpiredObjects);
|
||||
destroyStatement(discardExpiredProfiles);
|
||||
destroyStatement(getTotalObjects);
|
||||
destroyStatement(getTotalProfiles);
|
||||
destroyStatement(deleteSomeOldestObjects);
|
||||
destroyStatement(deleteSomeOldestProfiles);
|
||||
destroyStatement(querySkinByUUID);
|
||||
destroyStatement(queryProfileByUUID);
|
||||
destroyStatement(queryProfileByUsername);
|
||||
destroyStatement(cacheNewSkin);
|
||||
destroyStatement(cacheNewProfile);
|
||||
destroyStatement(cacheHasSkin);
|
||||
destroyStatement(cacheHasProfile);
|
||||
destroyStatement(cacheUpdateSkin);
|
||||
destroyStatement(cacheUpdateProfile);
|
||||
try {
|
||||
connection.close();
|
||||
EaglerXVelocity.logger().info("Successfully disconnected from database '{}'", uri);
|
||||
} catch (SQLException e) {
|
||||
EaglerXVelocity.logger().warn("Exception disconnecting from database '{}'!", uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 SimpleRateLimiter {
|
||||
|
||||
private long timer;
|
||||
private int count;
|
||||
|
||||
public SimpleRateLimiter() {
|
||||
timer = System.currentTimeMillis();
|
||||
count = 0;
|
||||
}
|
||||
|
||||
public boolean rateLimit(int maxPerMinute) {
|
||||
int t = 60000 / maxPerMinute;
|
||||
long millis = System.currentTimeMillis();
|
||||
int decr = (int)(millis - timer) / t;
|
||||
if(decr > 0) {
|
||||
timer += decr * t;
|
||||
count -= decr;
|
||||
if(count < 0) {
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
if(count >= maxPerMinute) {
|
||||
return false;
|
||||
}else {
|
||||
++count;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
|
||||
/**
|
||||
* 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 SkinPackets {
|
||||
|
||||
public static final int PACKET_MY_SKIN_PRESET = 0x01;
|
||||
public static final int PACKET_MY_SKIN_CUSTOM = 0x02;
|
||||
public static final int PACKET_GET_OTHER_SKIN = 0x03;
|
||||
public static final int PACKET_OTHER_SKIN_PRESET = 0x04;
|
||||
public static final int PACKET_OTHER_SKIN_CUSTOM = 0x05;
|
||||
public static final int PACKET_GET_SKIN_BY_URL = 0x06;
|
||||
|
||||
public static void processPacket(byte[] data, ConnectedPlayer sender, ISkinService skinService) throws IOException {
|
||||
if(data.length == 0) {
|
||||
throw new IOException("Zero-length packet recieved");
|
||||
}
|
||||
int packetId = (int)data[0] & 0xFF;
|
||||
try {
|
||||
switch(packetId) {
|
||||
case PACKET_GET_OTHER_SKIN:
|
||||
processGetOtherSkin(data, sender, skinService);
|
||||
break;
|
||||
case PACKET_GET_SKIN_BY_URL:
|
||||
processGetOtherSkinByURL(data, sender, skinService);
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unknown packet type " + packetId);
|
||||
}
|
||||
}catch(IOException ex) {
|
||||
throw ex;
|
||||
}catch(Throwable t) {
|
||||
throw new IOException("Unhandled exception handling packet type " + packetId, t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void processGetOtherSkin(byte[] data, ConnectedPlayer sender, ISkinService skinService) throws IOException {
|
||||
if(data.length != 17) {
|
||||
throw new IOException("Invalid length " + data.length + " for skin request packet");
|
||||
}
|
||||
UUID searchUUID = bytesToUUID(data, 1);
|
||||
skinService.processGetOtherSkin(searchUUID, sender);
|
||||
}
|
||||
|
||||
private static void processGetOtherSkinByURL(byte[] data, ConnectedPlayer sender, ISkinService skinService) throws IOException {
|
||||
if(data.length < 20) {
|
||||
throw new IOException("Invalid length " + data.length + " for skin request packet");
|
||||
}
|
||||
UUID searchUUID = bytesToUUID(data, 1);
|
||||
int urlLength = (data[17] << 8) | data[18];
|
||||
if(data.length < 19 + urlLength) {
|
||||
throw new IOException("Invalid length " + data.length + " for skin request packet with " + urlLength + " length URL");
|
||||
}
|
||||
String urlStr = bytesToAscii(data, 19, urlLength);
|
||||
urlStr = SkinService.sanitizeTextureURL(urlStr);
|
||||
if(urlStr == null) {
|
||||
throw new IOException("Invalid URL for skin request packet");
|
||||
}
|
||||
URL url;
|
||||
try {
|
||||
url = new URL(urlStr);
|
||||
}catch(MalformedURLException t) {
|
||||
throw new IOException("Invalid URL for skin request packet", t);
|
||||
}
|
||||
String host = url.getHost();
|
||||
if(EaglerXVelocity.getEagler().getConfig().isValidSkinHost(host)) {
|
||||
UUID validUUID = createEaglerURLSkinUUID(urlStr);
|
||||
if(!searchUUID.equals(validUUID)) {
|
||||
throw new IOException("Invalid generated UUID from skin URL");
|
||||
}
|
||||
skinService.processGetOtherSkin(searchUUID, urlStr, sender);
|
||||
}else {
|
||||
throw new IOException("Invalid host in skin packet: " + host);
|
||||
}
|
||||
}
|
||||
|
||||
public static void registerEaglerPlayer(UUID clientUUID, byte[] bs, ISkinService skinService) throws IOException {
|
||||
if(bs.length == 0) {
|
||||
throw new IOException("Zero-length packet recieved");
|
||||
}
|
||||
byte[] generatedPacket;
|
||||
int skinModel = -1;
|
||||
int packetType = (int)bs[0] & 0xFF;
|
||||
switch(packetType) {
|
||||
case PACKET_MY_SKIN_PRESET:
|
||||
if(bs.length != 5) {
|
||||
throw new IOException("Invalid length " + bs.length + " for preset skin packet");
|
||||
}
|
||||
generatedPacket = SkinPackets.makePresetResponse(clientUUID, (bs[1] << 24) | (bs[2] << 16) | (bs[3] << 8) | (bs[4] & 0xFF));
|
||||
break;
|
||||
case PACKET_MY_SKIN_CUSTOM:
|
||||
if(bs.length != 2 + 16384) {
|
||||
throw new IOException("Invalid length " + bs.length + " for custom skin packet");
|
||||
}
|
||||
setAlphaForChest(bs, (byte)255, 2);
|
||||
generatedPacket = SkinPackets.makeCustomResponse(clientUUID, (skinModel = (int)bs[1] & 0xFF), bs, 2, 16384);
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unknown skin packet type: " + packetType);
|
||||
}
|
||||
skinService.registerEaglercraftPlayer(clientUUID, generatedPacket, skinModel);
|
||||
}
|
||||
|
||||
public static void registerEaglerPlayerFallback(UUID clientUUID, ISkinService skinService) {
|
||||
int skinModel = (clientUUID.hashCode() & 1) != 0 ? 1 : 0;
|
||||
byte[] generatedPacket = SkinPackets.makePresetResponse(clientUUID, skinModel);
|
||||
skinService.registerEaglercraftPlayer(clientUUID, generatedPacket, skinModel);
|
||||
}
|
||||
|
||||
public static void setAlphaForChest(byte[] skin64x64, byte alpha, int offset) {
|
||||
if(skin64x64.length - offset != 16384) {
|
||||
throw new IllegalArgumentException("Skin is not 64x64!");
|
||||
}
|
||||
for(int y = 20; y < 32; ++y) {
|
||||
for(int x = 16; x < 40; ++x) {
|
||||
skin64x64[offset + ((y << 8) | (x << 2))] = alpha;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] makePresetResponse(UUID uuid) {
|
||||
return makePresetResponse(uuid, (uuid.hashCode() & 1) != 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
public static byte[] makePresetResponse(UUID uuid, int presetId) {
|
||||
byte[] ret = new byte[1 + 16 + 4];
|
||||
ret[0] = (byte)PACKET_OTHER_SKIN_PRESET;
|
||||
UUIDToBytes(uuid, ret, 1);
|
||||
ret[17] = (byte)(presetId >> 24);
|
||||
ret[18] = (byte)(presetId >> 16);
|
||||
ret[19] = (byte)(presetId >> 8);
|
||||
ret[20] = (byte)(presetId & 0xFF);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static byte[] makeCustomResponse(UUID uuid, int model, byte[] pixels) {
|
||||
return makeCustomResponse(uuid, model, pixels, 0, pixels.length);
|
||||
}
|
||||
|
||||
public static byte[] makeCustomResponse(UUID uuid, int model, byte[] pixels, int offset, int length) {
|
||||
byte[] ret = new byte[1 + 16 + 1 + length];
|
||||
ret[0] = (byte)PACKET_OTHER_SKIN_CUSTOM;
|
||||
UUIDToBytes(uuid, ret, 1);
|
||||
ret[17] = (byte)model;
|
||||
System.arraycopy(pixels, offset, ret, 18, length);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static UUID bytesToUUID(byte[] bytes, int off) {
|
||||
long msb = (((long) bytes[off] & 0xFFl) << 56l) | (((long) bytes[off + 1] & 0xFFl) << 48l)
|
||||
| (((long) bytes[off + 2] & 0xFFl) << 40l) | (((long) bytes[off + 3] & 0xFFl) << 32l)
|
||||
| (((long) bytes[off + 4] & 0xFFl) << 24l) | (((long) bytes[off + 5] & 0xFFl) << 16l)
|
||||
| (((long) bytes[off + 6] & 0xFFl) << 8l) | ((long) bytes[off + 7] & 0xFFl);
|
||||
long lsb = (((long) bytes[off + 8] & 0xFFl) << 56l) | (((long) bytes[off + 9] & 0xFFl) << 48l)
|
||||
| (((long) bytes[off + 10] & 0xFFl) << 40l) | (((long) bytes[off + 11] & 0xFFl) << 32l)
|
||||
| (((long) bytes[off + 12] & 0xFFl) << 24l) | (((long) bytes[off + 13] & 0xFFl) << 16l)
|
||||
| (((long) bytes[off + 14] & 0xFFl) << 8l) | ((long) bytes[off + 15] & 0xFFl);
|
||||
return new UUID(msb, lsb);
|
||||
}
|
||||
|
||||
private static final String hex = "0123456789abcdef";
|
||||
|
||||
public static String bytesToString(byte[] bytes, int off, int len) {
|
||||
char[] ret = new char[len << 1];
|
||||
for(int i = 0; i < len; ++i) {
|
||||
ret[i * 2] = hex.charAt((bytes[off + i] >> 4) & 0xF);
|
||||
ret[i * 2 + 1] = hex.charAt(bytes[off + i] & 0xF);
|
||||
}
|
||||
return new String(ret);
|
||||
}
|
||||
|
||||
public static String bytesToAscii(byte[] bytes, int off, int len) {
|
||||
char[] ret = new char[len];
|
||||
for(int i = 0; i < len; ++i) {
|
||||
ret[i] = (char)((int)bytes[off + i] & 0xFF);
|
||||
}
|
||||
return new String(ret);
|
||||
}
|
||||
|
||||
public static String bytesToAscii(byte[] bytes) {
|
||||
return bytesToAscii(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public static void UUIDToBytes(UUID uuid, byte[] bytes, int off) {
|
||||
long msb = uuid.getMostSignificantBits();
|
||||
long lsb = uuid.getLeastSignificantBits();
|
||||
bytes[off] = (byte)(msb >> 56l);
|
||||
bytes[off + 1] = (byte)(msb >> 48l);
|
||||
bytes[off + 2] = (byte)(msb >> 40l);
|
||||
bytes[off + 3] = (byte)(msb >> 32l);
|
||||
bytes[off + 4] = (byte)(msb >> 24l);
|
||||
bytes[off + 5] = (byte)(msb >> 16l);
|
||||
bytes[off + 6] = (byte)(msb >> 8l);
|
||||
bytes[off + 7] = (byte)(msb & 0xFFl);
|
||||
bytes[off + 8] = (byte)(lsb >> 56l);
|
||||
bytes[off + 9] = (byte)(lsb >> 48l);
|
||||
bytes[off + 10] = (byte)(lsb >> 40l);
|
||||
bytes[off + 11] = (byte)(lsb >> 32l);
|
||||
bytes[off + 12] = (byte)(lsb >> 24l);
|
||||
bytes[off + 13] = (byte)(lsb >> 16l);
|
||||
bytes[off + 14] = (byte)(lsb >> 8l);
|
||||
bytes[off + 15] = (byte)(lsb & 0xFFl);
|
||||
}
|
||||
|
||||
public static byte[] asciiString(String string) {
|
||||
byte[] str = new byte[string.length()];
|
||||
for(int i = 0; i < str.length; ++i) {
|
||||
str[i] = (byte)string.charAt(i);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public static UUID createEaglerURLSkinUUID(String skinUrl) {
|
||||
return UUID.nameUUIDFromBytes(asciiString("EaglercraftSkinURL:" + skinUrl));
|
||||
}
|
||||
|
||||
public static int getModelId(String modelName) {
|
||||
return "slim".equalsIgnoreCase(modelName) ? 1 : 0;
|
||||
}
|
||||
|
||||
public static byte[] rewriteUUID(UUID newUUID, byte[] pkt) {
|
||||
byte[] ret = new byte[pkt.length];
|
||||
System.arraycopy(pkt, 0, ret, 0, pkt.length);
|
||||
UUIDToBytes(newUUID, ret, 1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static byte[] rewriteUUIDModel(UUID newUUID, byte[] pkt, int model) {
|
||||
byte[] ret = new byte[pkt.length];
|
||||
System.arraycopy(pkt, 0, ret, 0, pkt.length);
|
||||
UUIDToBytes(newUUID, ret, 1);
|
||||
if(ret[0] == (byte)PACKET_OTHER_SKIN_CUSTOM) {
|
||||
ret[17] = (byte)model;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 SkinRescaler {
|
||||
|
||||
public static void convertToBytes(int[] imageIn, byte[] imageOut) {
|
||||
for(int i = 0, j, k; i < imageIn.length; ++i) {
|
||||
j = i << 2;
|
||||
k = imageIn[i];
|
||||
imageOut[j] = (byte)(k >> 24);
|
||||
imageOut[j + 1] = (byte)(k & 0xFF);
|
||||
imageOut[j + 2] = (byte)(k >> 8);
|
||||
imageOut[j + 3] = (byte)(k >> 16);
|
||||
}
|
||||
}
|
||||
|
||||
public static void convert64x32To64x64(int[] imageIn, byte[] imageOut) {
|
||||
copyRawPixels(imageIn, imageOut, 0, 0, 0, 0, 64, 32, 64, 64, false);
|
||||
copyRawPixels(imageIn, imageOut, 24, 48, 20, 52, 4, 16, 8, 20, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 28, 48, 24, 52, 8, 16, 12, 20, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 20, 52, 16, 64, 8, 20, 12, 32, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 24, 52, 20, 64, 4, 20, 8, 32, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 28, 52, 24, 64, 0, 20, 4, 32, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 32, 52, 28, 64, 12, 20, 16, 32, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 40, 48, 36, 52, 44, 16, 48, 20, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 44, 48, 40, 52, 48, 16, 52, 20, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 36, 52, 32, 64, 48, 20, 52, 32, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 40, 52, 36, 64, 44, 20, 48, 32, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 44, 52, 40, 64, 40, 20, 44, 32, 64, 64);
|
||||
copyRawPixels(imageIn, imageOut, 48, 52, 44, 64, 52, 20, 56, 32, 64, 64);
|
||||
}
|
||||
|
||||
private static void copyRawPixels(int[] imageIn, byte[] imageOut, int dx1, int dy1, int dx2, int dy2, int sx1,
|
||||
int sy1, int sx2, int sy2, int imgSrcWidth, int imgDstWidth) {
|
||||
if(dx1 > dx2) {
|
||||
copyRawPixels(imageIn, imageOut, sx1, sy1, dx2, dy1, sx2 - sx1, sy2 - sy1, imgSrcWidth, imgDstWidth, true);
|
||||
} else {
|
||||
copyRawPixels(imageIn, imageOut, sx1, sy1, dx1, dy1, sx2 - sx1, sy2 - sy1, imgSrcWidth, imgDstWidth, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyRawPixels(int[] imageIn, byte[] imageOut, int srcX, int srcY, int dstX, int dstY, int width,
|
||||
int height, int imgSrcWidth, int imgDstWidth, boolean flip) {
|
||||
int i, j;
|
||||
for(int y = 0; y < height; ++y) {
|
||||
for(int x = 0; x < width; ++x) {
|
||||
i = imageIn[(srcY + y) * imgSrcWidth + srcX + x];
|
||||
if(flip) {
|
||||
j = (dstY + y) * imgDstWidth + dstX + width - x - 1;
|
||||
}else {
|
||||
j = (dstY + y) * imgDstWidth + dstX + x;
|
||||
}
|
||||
j = j << 2;
|
||||
imageOut[j] = (byte)(i >> 24);
|
||||
imageOut[j + 1] = (byte)(i & 0xFF);
|
||||
imageOut[j + 2] = (byte)(i >> 8);
|
||||
imageOut[j + 3] = (byte)(i >> 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,119 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.skins;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.MultimapBuilder;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 SkinServiceOffline implements ISkinService {
|
||||
|
||||
public static final int masterRateLimitPerPlayer = 250;
|
||||
|
||||
private static class CachedSkin {
|
||||
|
||||
protected final UUID uuid;
|
||||
protected final byte[] packet;
|
||||
|
||||
protected CachedSkin(UUID uuid, byte[] packet) {
|
||||
this.uuid = uuid;
|
||||
this.packet = packet;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final Map<UUID, CachedSkin> skinCache = new HashMap();
|
||||
|
||||
private final Multimap<UUID, UUID> onlinePlayersFromTexturesMap = MultimapBuilder.hashKeys().hashSetValues().build();
|
||||
|
||||
public void init(String uri, String driverClass, String driverPath, int keepObjectsDays, int keepProfilesDays,
|
||||
int maxObjects, int maxProfiles) {
|
||||
synchronized(skinCache) {
|
||||
skinCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void processGetOtherSkin(UUID searchUUID, ConnectedPlayer sender) {
|
||||
if(EaglerPipeline.getEaglerHandle(sender).skinLookupRateLimiter.rateLimit(masterRateLimitPerPlayer)) {
|
||||
CachedSkin cached;
|
||||
synchronized(skinCache) {
|
||||
cached = skinCache.get(searchUUID);
|
||||
}
|
||||
if(cached != null) {
|
||||
sender.sendPluginMessage(SkinService.CHANNEL, cached.packet);
|
||||
}else {
|
||||
sender.sendPluginMessage(SkinService.CHANNEL, SkinPackets.makePresetResponse(searchUUID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void processGetOtherSkin(UUID searchUUID, String skinURL, ConnectedPlayer sender) {
|
||||
Collection<UUID> uuids;
|
||||
synchronized(onlinePlayersFromTexturesMap) {
|
||||
uuids = onlinePlayersFromTexturesMap.get(searchUUID);
|
||||
}
|
||||
if(uuids.size() > 0) {
|
||||
CachedSkin cached;
|
||||
synchronized(skinCache) {
|
||||
Iterator<UUID> uuidItr = uuids.iterator();
|
||||
while(uuidItr.hasNext()) {
|
||||
cached = skinCache.get(uuidItr.next());
|
||||
if(cached != null) {
|
||||
sender.sendPluginMessage(SkinService.CHANNEL, SkinPackets.rewriteUUID(searchUUID, cached.packet));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sender.sendPluginMessage(SkinService.CHANNEL, SkinPackets.makePresetResponse(searchUUID));
|
||||
}
|
||||
|
||||
public void registerEaglercraftPlayer(UUID clientUUID, byte[] generatedPacket, int modelId) {
|
||||
synchronized(skinCache) {
|
||||
skinCache.put(clientUUID, new CachedSkin(clientUUID, generatedPacket));
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterPlayer(UUID clientUUID) {
|
||||
synchronized(skinCache) {
|
||||
skinCache.remove(clientUUID);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerTextureToPlayerAssociation(UUID textureUUID, UUID playerUUID) {
|
||||
synchronized(onlinePlayersFromTexturesMap) {
|
||||
onlinePlayersFromTexturesMap.put(textureUUID, playerUUID);
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() {
|
||||
// no
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
synchronized(skinCache) {
|
||||
skinCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.sqlite;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Driver;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2023 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 EaglerDrivers {
|
||||
|
||||
private static Driver initializeDriver(String address, String driverClass) {
|
||||
URLClassLoader classLoader = driversJARs.get(address);
|
||||
if(classLoader == null) {
|
||||
File driver;
|
||||
if(address.equalsIgnoreCase("internal")) {
|
||||
driver = new File(EaglerXVelocity.getEagler().getDataFolder(), "drivers/sqlite-jdbc.jar");
|
||||
driver.getParentFile().mkdirs();
|
||||
if(!driver.exists()) {
|
||||
try {
|
||||
URL u = new URL("https://repo1.maven.org/maven2/org/xerial/sqlite-jdbc/3.45.0.0/sqlite-jdbc-3.45.0.0.jar");
|
||||
EaglerXVelocity.logger().info("Downloading from maven: " + u.toString());
|
||||
copyURLToFile(u, driver);
|
||||
} catch (Throwable ex) {
|
||||
EaglerXVelocity.logger().error("Could not download sqlite-jdbc.jar from repo1.maven.org!");
|
||||
EaglerXVelocity.logger().error("Please download \"org.xerial:sqlite-jdbc:3.45.0.0\" jar to file: " + driver.getAbsolutePath());
|
||||
throw new ExceptionInInitializerError(ex);
|
||||
}
|
||||
}
|
||||
}else {
|
||||
driver = new File(address);
|
||||
}
|
||||
URL driverURL;
|
||||
try {
|
||||
driverURL = driver.toURI().toURL();
|
||||
}catch(MalformedURLException ex) {
|
||||
EaglerXVelocity.logger().error("Invalid JDBC driver path: " + address);
|
||||
throw new ExceptionInInitializerError(ex);
|
||||
}
|
||||
classLoader = URLClassLoader.newInstance(new URL[] { driverURL }, ClassLoader.getSystemClassLoader());
|
||||
driversJARs.put(address, classLoader);
|
||||
}
|
||||
|
||||
Class loadedDriver;
|
||||
try {
|
||||
loadedDriver = classLoader.loadClass(driverClass);
|
||||
}catch(ClassNotFoundException ex) {
|
||||
try {
|
||||
classLoader.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
EaglerXVelocity.logger().error("Could not find JDBC driver class: " + driverClass);
|
||||
throw new ExceptionInInitializerError(ex);
|
||||
}
|
||||
Driver sqlDriver = null;
|
||||
try {
|
||||
sqlDriver = (Driver) loadedDriver.newInstance();
|
||||
}catch(Throwable ex) {
|
||||
try {
|
||||
classLoader.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
EaglerXVelocity.logger().error("Could not initialize JDBC driver class: " + driverClass);
|
||||
throw new ExceptionInInitializerError(ex);
|
||||
}
|
||||
|
||||
return sqlDriver;
|
||||
}
|
||||
|
||||
private static final Map<String, URLClassLoader> driversJARs = new HashMap();
|
||||
private static final Map<String, Driver> driversDrivers = new HashMap();
|
||||
|
||||
public static Connection connectToDatabase(String address, String driverClass, String driverPath, Properties props)
|
||||
throws SQLException {
|
||||
if(driverClass.equalsIgnoreCase("internal")) {
|
||||
driverClass = "org.sqlite.JDBC";
|
||||
}
|
||||
if(driverPath == null) {
|
||||
try {
|
||||
Class.forName(driverClass);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("Driver class not found in JRE: " + driverClass, e);
|
||||
}
|
||||
return DriverManager.getConnection(address, props);
|
||||
}else {
|
||||
String driverMapPath = "" + driverPath + "?" + driverClass;
|
||||
Driver dv = driversDrivers.get(driverMapPath);
|
||||
if(dv == null) {
|
||||
dv = initializeDriver(driverPath, driverClass);
|
||||
driversDrivers.put(driverMapPath, dv);
|
||||
}
|
||||
return dv.connect(address, props);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyURLToFile(URL url, File file) throws IOException {
|
||||
try(InputStream is = url.openStream()) {
|
||||
try(OutputStream os = new FileOutputStream(file)) {
|
||||
byte[] buf = new byte[32768];
|
||||
int i;
|
||||
while((i = is.read(buf)) != -1) {
|
||||
os.write(buf, 0, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.voice;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022 ayunami2000. 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 ExpiringSet<T> extends HashSet<T> {
|
||||
private final long expiration;
|
||||
private final ExpiringEvent<T> event;
|
||||
|
||||
private final Map<T, Long> timestamps = new HashMap<>();
|
||||
|
||||
public ExpiringSet(long expiration) {
|
||||
this.expiration = expiration;
|
||||
this.event = null;
|
||||
}
|
||||
|
||||
public ExpiringSet(long expiration, ExpiringEvent<T> event) {
|
||||
this.expiration = expiration;
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
public interface ExpiringEvent<T> {
|
||||
void onExpiration(T item);
|
||||
}
|
||||
|
||||
public void checkForExpirations() {
|
||||
Iterator<T> iterator = this.timestamps.keySet().iterator();
|
||||
long now = System.currentTimeMillis();
|
||||
while (iterator.hasNext()) {
|
||||
T element = iterator.next();
|
||||
if (super.contains(element)) {
|
||||
if (this.timestamps.get(element) + this.expiration < now) {
|
||||
if (this.event != null) this.event.onExpiration(element);
|
||||
iterator.remove();
|
||||
super.remove(element);
|
||||
}
|
||||
} else {
|
||||
iterator.remove();
|
||||
super.remove(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean add(T o) {
|
||||
checkForExpirations();
|
||||
boolean success = super.add(o);
|
||||
if (success) timestamps.put(o, System.currentTimeMillis());
|
||||
return success;
|
||||
}
|
||||
|
||||
public boolean remove(Object o) {
|
||||
checkForExpirations();
|
||||
boolean success = super.remove(o);
|
||||
if (success) timestamps.remove(o);
|
||||
return success;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.timestamps.clear();
|
||||
super.clear();
|
||||
}
|
||||
|
||||
public boolean contains(Object o) {
|
||||
checkForExpirations();
|
||||
return super.contains(o);
|
||||
}
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.voice;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.server.EaglerPipeline;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2022-2024 lax1dude, ayunami2000. 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 VoiceServerImpl {
|
||||
|
||||
private final ServerInfo server;
|
||||
private final byte[] iceServersPacket;
|
||||
|
||||
private final Map<UUID, ConnectedPlayer> voicePlayers = new HashMap<>();
|
||||
private final Map<UUID, ExpiringSet<UUID>> voiceRequests = new HashMap<>();
|
||||
private final Set<VoicePair> voicePairs = new HashSet<>();
|
||||
|
||||
private static final int VOICE_CONNECT_RATELIMIT = 15;
|
||||
|
||||
private static class VoicePair {
|
||||
|
||||
private final UUID uuid1;
|
||||
private final UUID uuid2;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return uuid1.hashCode() ^ uuid2.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
VoicePair other = (VoicePair) obj;
|
||||
return (uuid1.equals(other.uuid1) && uuid2.equals(other.uuid2))
|
||||
|| (uuid1.equals(other.uuid2) && uuid2.equals(other.uuid1));
|
||||
}
|
||||
|
||||
private VoicePair(UUID uuid1, UUID uuid2) {
|
||||
this.uuid1 = uuid1;
|
||||
this.uuid2 = uuid2;
|
||||
}
|
||||
|
||||
private boolean anyEquals(UUID uuid) {
|
||||
return uuid1.equals(uuid) || uuid2.equals(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
VoiceServerImpl(ServerInfo server, byte[] iceServersPacket) {
|
||||
this.server = server;
|
||||
this.iceServersPacket = iceServersPacket;
|
||||
}
|
||||
|
||||
public void handlePlayerLoggedIn(ConnectedPlayer player) {
|
||||
player.sendPluginMessage(VoiceService.CHANNEL, iceServersPacket);
|
||||
}
|
||||
|
||||
public void handlePlayerLoggedOut(ConnectedPlayer player) {
|
||||
removeUser(player.getUniqueId());
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeRequest(UUID player, ConnectedPlayer sender) {
|
||||
synchronized (voicePlayers) {
|
||||
UUID senderUUID = sender.getUniqueId();
|
||||
if (senderUUID.equals(player))
|
||||
return; // prevent duplicates
|
||||
if (!voicePlayers.containsKey(senderUUID))
|
||||
return;
|
||||
ConnectedPlayer targetPlayerCon = voicePlayers.get(player);
|
||||
if (targetPlayerCon == null)
|
||||
return;
|
||||
VoicePair newPair = new VoicePair(player, senderUUID);
|
||||
if (voicePairs.contains(newPair))
|
||||
return; // already paired
|
||||
ExpiringSet<UUID> senderRequestSet = voiceRequests.get(senderUUID);
|
||||
if (senderRequestSet == null) {
|
||||
voiceRequests.put(senderUUID, senderRequestSet = new ExpiringSet<>(2000));
|
||||
}
|
||||
if (!senderRequestSet.add(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if other has requested earlier
|
||||
ExpiringSet<UUID> theSet;
|
||||
if ((theSet = voiceRequests.get(player)) != null && theSet.contains(senderUUID)) {
|
||||
theSet.remove(senderUUID);
|
||||
if (theSet.isEmpty())
|
||||
voiceRequests.remove(player);
|
||||
senderRequestSet.remove(player);
|
||||
if (senderRequestSet.isEmpty())
|
||||
voiceRequests.remove(senderUUID);
|
||||
// send each other add data
|
||||
voicePairs.add(newPair);
|
||||
targetPlayerCon.sendPluginMessage(VoiceService.CHANNEL,
|
||||
VoiceSignalPackets.makeVoiceSignalPacketConnect(senderUUID, false));
|
||||
sender.sendPluginMessage(VoiceService.CHANNEL,
|
||||
VoiceSignalPackets.makeVoiceSignalPacketConnect(player, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeConnect(ConnectedPlayer sender) {
|
||||
if(!EaglerPipeline.getEaglerHandle(sender).voiceConnectRateLimiter.rateLimit(VOICE_CONNECT_RATELIMIT)) {
|
||||
return;
|
||||
}
|
||||
synchronized (voicePlayers) {
|
||||
if (voicePlayers.containsKey(sender.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
boolean hasNoOtherPlayers = voicePlayers.isEmpty();
|
||||
voicePlayers.put(sender.getUniqueId(), sender);
|
||||
if (hasNoOtherPlayers) {
|
||||
return;
|
||||
}
|
||||
byte[] packetToBroadcast = VoiceSignalPackets.makeVoiceSignalPacketGlobal(voicePlayers.values());
|
||||
for (ConnectedPlayer userCon : voicePlayers.values()) {
|
||||
userCon.sendPluginMessage(VoiceService.CHANNEL, packetToBroadcast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeICE(UUID player, String str, ConnectedPlayer sender) {
|
||||
ConnectedPlayer pass;
|
||||
VoicePair pair = new VoicePair(player, sender.getUniqueId());
|
||||
synchronized (voicePlayers) {
|
||||
pass = voicePairs.contains(pair) ? voicePlayers.get(player) : null;
|
||||
}
|
||||
if (pass != null) {
|
||||
pass.sendPluginMessage(VoiceService.CHANNEL,
|
||||
VoiceSignalPackets.makeVoiceSignalPacketICE(sender.getUniqueId(), str));
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeDesc(UUID player, String str, ConnectedPlayer sender) {
|
||||
ConnectedPlayer pass;
|
||||
VoicePair pair = new VoicePair(player, sender.getUniqueId());
|
||||
synchronized (voicePlayers) {
|
||||
pass = voicePairs.contains(pair) ? voicePlayers.get(player) : null;
|
||||
}
|
||||
if (pass != null) {
|
||||
pass.sendPluginMessage(VoiceService.CHANNEL,
|
||||
VoiceSignalPackets.makeVoiceSignalPacketDesc(sender.getUniqueId(), str));
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeDisconnect(UUID player, ConnectedPlayer sender) {
|
||||
if (player != null) {
|
||||
synchronized (voicePlayers) {
|
||||
if (!voicePlayers.containsKey(player)) {
|
||||
return;
|
||||
}
|
||||
byte[] userDisconnectPacket = null;
|
||||
Iterator<VoicePair> pairsItr = voicePairs.iterator();
|
||||
while (pairsItr.hasNext()) {
|
||||
VoicePair voicePair = pairsItr.next();
|
||||
UUID target = null;
|
||||
if (voicePair.uuid1.equals(player)) {
|
||||
target = voicePair.uuid2;
|
||||
} else if (voicePair.uuid2.equals(player)) {
|
||||
target = voicePair.uuid1;
|
||||
}
|
||||
if (target != null) {
|
||||
pairsItr.remove();
|
||||
ConnectedPlayer conn = voicePlayers.get(target);
|
||||
if (conn != null) {
|
||||
if (userDisconnectPacket == null) {
|
||||
userDisconnectPacket = VoiceSignalPackets.makeVoiceSignalPacketDisconnect(player);
|
||||
}
|
||||
conn.sendPluginMessage(VoiceService.CHANNEL, userDisconnectPacket);
|
||||
}
|
||||
sender.sendPluginMessage(VoiceService.CHANNEL,
|
||||
VoiceSignalPackets.makeVoiceSignalPacketDisconnect(target));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
removeUser(sender.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
public void removeUser(UUID user) {
|
||||
synchronized (voicePlayers) {
|
||||
if (voicePlayers.remove(user) == null) {
|
||||
return;
|
||||
}
|
||||
voiceRequests.remove(user);
|
||||
if (voicePlayers.size() > 0) {
|
||||
byte[] voicePlayersPkt = VoiceSignalPackets.makeVoiceSignalPacketGlobal(voicePlayers.values());
|
||||
for (ConnectedPlayer userCon : voicePlayers.values()) {
|
||||
if (!user.equals(userCon.getUniqueId())) {
|
||||
userCon.sendPluginMessage(VoiceService.CHANNEL, voicePlayersPkt);
|
||||
}
|
||||
}
|
||||
}
|
||||
byte[] userDisconnectPacket = null;
|
||||
Iterator<VoicePair> pairsItr = voicePairs.iterator();
|
||||
while (pairsItr.hasNext()) {
|
||||
VoicePair voicePair = pairsItr.next();
|
||||
UUID target = null;
|
||||
if (voicePair.uuid1.equals(user)) {
|
||||
target = voicePair.uuid2;
|
||||
} else if (voicePair.uuid2.equals(user)) {
|
||||
target = voicePair.uuid1;
|
||||
}
|
||||
if (target != null) {
|
||||
pairsItr.remove();
|
||||
if (voicePlayers.size() > 0) {
|
||||
ConnectedPlayer conn = voicePlayers.get(target);
|
||||
if (conn != null) {
|
||||
if (userDisconnectPacket == null) {
|
||||
userDisconnectPacket = VoiceSignalPackets.makeVoiceSignalPacketDisconnect(user);
|
||||
}
|
||||
conn.sendPluginMessage(VoiceService.CHANNEL, userDisconnectPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.voice;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity;
|
||||
import net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.config.EaglerVelocityConfig;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 VoiceService {
|
||||
|
||||
public static final ChannelIdentifier CHANNEL = new LegacyChannelIdentifier("EAG|Voice-1.8");
|
||||
|
||||
private final Map<String, VoiceServerImpl> serverMap = new HashMap();
|
||||
private final byte[] disableVoicePacket;
|
||||
|
||||
public VoiceService(EaglerVelocityConfig conf) {
|
||||
this.disableVoicePacket = VoiceSignalPackets.makeVoiceSignalPacketAllowed(false, null);
|
||||
String[] iceServers = conf.getICEServers().toArray(new String[conf.getICEServers().size()]);
|
||||
byte[] iceServersPacket = VoiceSignalPackets.makeVoiceSignalPacketAllowed(true, iceServers);
|
||||
Collection<RegisteredServer> servers = EaglerXVelocity.proxy().getAllServers();
|
||||
for(RegisteredServer s : servers) {
|
||||
ServerInfo inf = s.getServerInfo();
|
||||
if(!conf.getDisableVoiceOnServersSet().contains(inf.getName())) {
|
||||
serverMap.put(inf.getName(), new VoiceServerImpl(inf, iceServersPacket));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handlePlayerLoggedIn(ConnectedPlayer player) {
|
||||
|
||||
}
|
||||
|
||||
public void handlePlayerLoggedOut(ConnectedPlayer player) {
|
||||
for(VoiceServerImpl svr : serverMap.values()) {
|
||||
svr.handlePlayerLoggedOut(player);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleServerConnected(ConnectedPlayer player, ServerInfo server) {
|
||||
VoiceServerImpl svr = serverMap.get(server.getName());
|
||||
if(svr != null) {
|
||||
svr.handlePlayerLoggedIn(player);
|
||||
}else {
|
||||
player.sendPluginMessage(CHANNEL, disableVoicePacket);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleServerDisconnected(ConnectedPlayer player, ServerInfo server) {
|
||||
VoiceServerImpl svr = serverMap.get(server.getName());
|
||||
if(svr != null) {
|
||||
svr.handlePlayerLoggedOut(player);
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeRequest(UUID player, ConnectedPlayer sender) {
|
||||
if(sender.getConnectedServer() != null) {
|
||||
VoiceServerImpl svr = serverMap.get(sender.getConnectedServer().getServerInfo().getName());
|
||||
if(svr != null) {
|
||||
svr.handleVoiceSignalPacketTypeRequest(player, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeConnect(ConnectedPlayer sender) {
|
||||
if(sender.getConnectedServer() != null) {
|
||||
VoiceServerImpl svr = serverMap.get(sender.getConnectedServer().getServerInfo().getName());
|
||||
if(svr != null) {
|
||||
svr.handleVoiceSignalPacketTypeConnect(sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeICE(UUID player, String str, ConnectedPlayer sender) {
|
||||
if(sender.getConnectedServer() != null) {
|
||||
VoiceServerImpl svr = serverMap.get(sender.getConnectedServer().getServerInfo().getName());
|
||||
if(svr != null) {
|
||||
svr.handleVoiceSignalPacketTypeICE(player, str, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeDesc(UUID player, String str, ConnectedPlayer sender) {
|
||||
if(sender.getConnectedServer() != null) {
|
||||
VoiceServerImpl svr = serverMap.get(sender.getConnectedServer().getServerInfo().getName());
|
||||
if(svr != null) {
|
||||
svr.handleVoiceSignalPacketTypeDesc(player, str, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handleVoiceSignalPacketTypeDisconnect(UUID player, ConnectedPlayer sender) {
|
||||
if(sender.getConnectedServer() != null) {
|
||||
VoiceServerImpl svr = serverMap.get(sender.getConnectedServer().getServerInfo().getName());
|
||||
if(svr != null) {
|
||||
svr.handleVoiceSignalPacketTypeDisconnect(player, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
package net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.voice;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
|
||||
/**
|
||||
* Copyright (c) 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 VoiceSignalPackets {
|
||||
|
||||
static final int VOICE_SIGNAL_ALLOWED = 0;
|
||||
static final int VOICE_SIGNAL_REQUEST = 0;
|
||||
static final int VOICE_SIGNAL_CONNECT = 1;
|
||||
static final int VOICE_SIGNAL_DISCONNECT = 2;
|
||||
static final int VOICE_SIGNAL_ICE = 3;
|
||||
static final int VOICE_SIGNAL_DESC = 4;
|
||||
static final int VOICE_SIGNAL_GLOBAL = 5;
|
||||
|
||||
public static void processPacket(byte[] data, ConnectedPlayer sender, VoiceService voiceService) throws IOException {
|
||||
int packetId = -1;
|
||||
if(data.length == 0) {
|
||||
throw new IOException("Zero-length packet recieved");
|
||||
}
|
||||
try {
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(data).writerIndex(data.length);
|
||||
packetId = buffer.readUnsignedByte();
|
||||
switch(packetId) {
|
||||
case VOICE_SIGNAL_REQUEST: {
|
||||
voiceService.handleVoiceSignalPacketTypeRequest(ProtocolUtils.readUuid(buffer), sender);
|
||||
break;
|
||||
}
|
||||
case VOICE_SIGNAL_CONNECT: {
|
||||
voiceService.handleVoiceSignalPacketTypeConnect(sender);
|
||||
break;
|
||||
}
|
||||
case VOICE_SIGNAL_ICE: {
|
||||
voiceService.handleVoiceSignalPacketTypeICE(ProtocolUtils.readUuid(buffer), ProtocolUtils.readString(buffer, 32767), sender);
|
||||
break;
|
||||
}
|
||||
case VOICE_SIGNAL_DESC: {
|
||||
voiceService.handleVoiceSignalPacketTypeDesc(ProtocolUtils.readUuid(buffer), ProtocolUtils.readString(buffer, 32767), sender);
|
||||
break;
|
||||
}
|
||||
case VOICE_SIGNAL_DISCONNECT: {
|
||||
voiceService.handleVoiceSignalPacketTypeDisconnect(buffer.readableBytes() > 0 ? ProtocolUtils.readUuid(buffer) : null, sender);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new IOException("Unknown packet type " + packetId);
|
||||
}
|
||||
}
|
||||
if(buffer.readableBytes() > 0) {
|
||||
throw new IOException("Voice packet is too long!");
|
||||
}
|
||||
}catch(IOException ex) {
|
||||
throw ex;
|
||||
}catch(Throwable t) {
|
||||
throw new IOException("Unhandled exception handling voice packet type " + packetId, t);
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] makeVoiceSignalPacketAllowed(boolean allowed, String[] iceServers) {
|
||||
if (iceServers == null) {
|
||||
byte[] ret = new byte[2];
|
||||
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(ret).writerIndex(0);
|
||||
wrappedBuffer.writeByte(VOICE_SIGNAL_ALLOWED);
|
||||
wrappedBuffer.writeBoolean(allowed);
|
||||
return ret;
|
||||
}
|
||||
byte[][] iceServersBytes = new byte[iceServers.length][];
|
||||
int totalLen = 2 + getVarIntSize(iceServers.length);
|
||||
for(int i = 0; i < iceServers.length; ++i) {
|
||||
byte[] b = iceServersBytes[i] = iceServers[i].getBytes(StandardCharsets.UTF_8);
|
||||
totalLen += getVarIntSize(b.length) + b.length;
|
||||
}
|
||||
byte[] ret = new byte[totalLen];
|
||||
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(ret).writerIndex(0);
|
||||
wrappedBuffer.writeByte(VOICE_SIGNAL_ALLOWED);
|
||||
wrappedBuffer.writeBoolean(allowed);
|
||||
ProtocolUtils.writeVarInt(wrappedBuffer, iceServersBytes.length);
|
||||
for(int i = 0; i < iceServersBytes.length; ++i) {
|
||||
byte[] b = iceServersBytes[i];
|
||||
ProtocolUtils.writeVarInt(wrappedBuffer, b.length);
|
||||
wrappedBuffer.writeBytes(b);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static byte[] makeVoiceSignalPacketGlobal(Collection<ConnectedPlayer> users) {
|
||||
int cnt = users.size();
|
||||
byte[][] displayNames = new byte[cnt][];
|
||||
int i = 0;
|
||||
for(ConnectedPlayer user : users) {
|
||||
String name = user.getUsername();
|
||||
if(name.length() > 16) name = name.substring(0, 16);
|
||||
displayNames[i++] = name.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
int totalLength = 1 + getVarIntSize(cnt) + (cnt << 4);
|
||||
for(i = 0; i < cnt; ++i) {
|
||||
totalLength += getVarIntSize(displayNames[i].length) + displayNames[i].length;
|
||||
}
|
||||
byte[] ret = new byte[totalLength];
|
||||
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(ret).writerIndex(0);
|
||||
wrappedBuffer.writeByte(VOICE_SIGNAL_GLOBAL);
|
||||
ProtocolUtils.writeVarInt(wrappedBuffer, cnt);
|
||||
for(ConnectedPlayer user : users) {
|
||||
ProtocolUtils.writeUuid(wrappedBuffer, user.getUniqueId());
|
||||
}
|
||||
for(i = 0; i < cnt; ++i) {
|
||||
ProtocolUtils.writeVarInt(wrappedBuffer, displayNames[i].length);
|
||||
wrappedBuffer.writeBytes(displayNames[i]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static byte[] makeVoiceSignalPacketConnect(UUID player, boolean offer) {
|
||||
byte[] ret = new byte[18];
|
||||
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(ret).writerIndex(0);
|
||||
wrappedBuffer.writeByte(VOICE_SIGNAL_CONNECT);
|
||||
ProtocolUtils.writeUuid(wrappedBuffer, player);
|
||||
wrappedBuffer.writeBoolean(offer);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static byte[] makeVoiceSignalPacketConnectAnnounce(UUID player) {
|
||||
byte[] ret = new byte[17];
|
||||
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(ret).writerIndex(0);
|
||||
wrappedBuffer.writeByte(VOICE_SIGNAL_CONNECT);
|
||||
ProtocolUtils.writeUuid(wrappedBuffer, player);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static byte[] makeVoiceSignalPacketDisconnect(UUID player) {
|
||||
if(player == null) {
|
||||
return new byte[] { (byte)VOICE_SIGNAL_DISCONNECT };
|
||||
}
|
||||
byte[] ret = new byte[17];
|
||||
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(ret).writerIndex(0);
|
||||
wrappedBuffer.writeByte(VOICE_SIGNAL_DISCONNECT);
|
||||
ProtocolUtils.writeUuid(wrappedBuffer, player);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static byte[] makeVoiceSignalPacketICE(UUID player, String str) {
|
||||
byte[] strBytes = str.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] ret = new byte[17 + getVarIntSize(strBytes.length) + strBytes.length];
|
||||
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(ret).writerIndex(0);
|
||||
wrappedBuffer.writeByte(VOICE_SIGNAL_ICE);
|
||||
ProtocolUtils.writeUuid(wrappedBuffer, player);
|
||||
ProtocolUtils.writeVarInt(wrappedBuffer, strBytes.length);
|
||||
wrappedBuffer.writeBytes(strBytes);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static byte[] makeVoiceSignalPacketDesc(UUID player, String str) {
|
||||
byte[] strBytes = str.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] ret = new byte[17 + getVarIntSize(strBytes.length) + strBytes.length];
|
||||
ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(ret).writerIndex(0);
|
||||
wrappedBuffer.writeByte(VOICE_SIGNAL_DESC);
|
||||
ProtocolUtils.writeUuid(wrappedBuffer, player);
|
||||
ProtocolUtils.writeVarInt(wrappedBuffer, strBytes.length);
|
||||
wrappedBuffer.writeBytes(strBytes);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static int getVarIntSize(int input) {
|
||||
for (int i = 1; i < 5; ++i) {
|
||||
if ((input & -1 << i * 7) == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
enable_authentication_system: true
|
||||
use_onboard_eaglerx_system: true
|
||||
auth_db_uri: 'jdbc:sqlite:eaglercraft_auths.db'
|
||||
sql_driver_class: 'internal'
|
||||
sql_driver_path: 'internal'
|
||||
password_prompt_screen_text: 'Enter your password to join:'
|
||||
wrong_password_screen_text: 'Password Incorrect!'
|
||||
not_registered_screen_text: 'You are not registered on this server!'
|
||||
eagler_command_name: 'eagler'
|
||||
use_register_command_text: '&aUse /eagler to set an Eaglercraft password on this account'
|
||||
use_change_command_text: '&bUse /eagler to change your Eaglercraft password'
|
||||
command_success_text: '&bYour eagler password was changed successfully.'
|
||||
last_eagler_login_message: 'Your last Eaglercraft login was on $date from $ip'
|
||||
too_many_registrations_message: '&cThe maximum number of registrations has been reached for your IP address'
|
||||
need_vanilla_to_register_message: '&cYou need to log in with a vanilla account to use this command'
|
||||
override_eagler_to_vanilla_skins: false
|
||||
max_registration_per_ip: -1
|
@ -0,0 +1,180 @@
|
||||
{
|
||||
"text/html": {
|
||||
"files": [ "html", "htm", "shtml" ],
|
||||
"expires": 3600,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"application/javascript": {
|
||||
"files": [ "js" ],
|
||||
"expires": 3600,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"application/octet-stream": {
|
||||
"files": [ "epk" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"text/css": {
|
||||
"files": [ "css" ],
|
||||
"expires": 14400,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"text/xml": {
|
||||
"files": [ "xml" ],
|
||||
"expires": 3600,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"text/plain": {
|
||||
"files": [ "txt" ],
|
||||
"expires": 3600,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"image/png": {
|
||||
"files": [ "png" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"image/jpeg": {
|
||||
"files": [ "jpeg", "jpg", "jfif" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"image/gif": {
|
||||
"files": [ "gif" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"image/webp": {
|
||||
"files": [ "webp" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"image/svg+xml": {
|
||||
"files": [ "svg", "svgz" ],
|
||||
"expires": 14400,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"image/tiff": {
|
||||
"files": [ "tiff", "tif" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"image/avif": {
|
||||
"files": [ "avif" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"image/x-ms-bmp": {
|
||||
"files": [ "bmp" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"image/x-icon": {
|
||||
"files": [ "ico" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"image/woff": {
|
||||
"files": [ "woff" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"image/woff2": {
|
||||
"files": [ "woff2" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"application/json": {
|
||||
"files": [ "json" ],
|
||||
"expires": 3600,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"application/pdf": {
|
||||
"files": [ "pdf" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"application/rtf": {
|
||||
"files": [ "rtf" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"application/java-archive": {
|
||||
"files": [ "jar", "war", "ear" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"application/wasm": {
|
||||
"files": [ "wasm" ],
|
||||
"expires": 3600
|
||||
},
|
||||
"application/xhtml+xml": {
|
||||
"files": [ "xhtml" ],
|
||||
"expires": 3600,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"application/zip": {
|
||||
"files": [ "zip" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"audio/midi": {
|
||||
"files": [ "mid", "midi", "kar" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"audio/mpeg": {
|
||||
"files": [ "mp3" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"audio/ogg": {
|
||||
"files": [ "ogg" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"audio/x-m4a": {
|
||||
"files": [ "m4a" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"application/atom+xml": {
|
||||
"files": [ "atom" ],
|
||||
"expires": 3600,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"application/rss+xml": {
|
||||
"files": [ "rss" ],
|
||||
"expires": 3600,
|
||||
"charset": "utf-8"
|
||||
},
|
||||
"application/x-shockwave-flash": {
|
||||
"files": [ "swf" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/3gpp": {
|
||||
"files": [ "3gpp", "3gp" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/mp4": {
|
||||
"files": [ "mp4" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/mpeg": {
|
||||
"files": [ "mpeg", "mpg" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/quicktime": {
|
||||
"files": [ "mov" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/webm": {
|
||||
"files": [ "webm" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/x-motion-jpeg": {
|
||||
"files": [ "mjpg" ],
|
||||
"expires": 14400
|
||||
},
|
||||
"video/x-flv": {
|
||||
"files": [ "flv" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/x-m4v": {
|
||||
"files": [ "m4v" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/x-mng": {
|
||||
"files": [ "3mng" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/x-ms-wmv": {
|
||||
"files": [ "wmv" ],
|
||||
"expires": 43200
|
||||
},
|
||||
"video/x-msvideo": {
|
||||
"files": [ "avi" ],
|
||||
"expires": 43200
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
voice_stun_servers:
|
||||
- 'stun:stun.l.google.com:19302'
|
||||
- 'stun:stun1.l.google.com:19302'
|
||||
- 'stun:stun2.l.google.com:19302'
|
||||
- 'stun:stun3.l.google.com:19302'
|
||||
- 'stun:stun4.l.google.com:19302'
|
||||
- 'stun:openrelay.metered.ca:80'
|
||||
voice_turn_servers:
|
||||
openrelay1:
|
||||
url: 'turn:openrelay.metered.ca:80'
|
||||
username: 'openrelayproject'
|
||||
password: 'openrelayproject'
|
||||
openrelay2:
|
||||
url: 'turn:openrelay.metered.ca:443'
|
||||
username: 'openrelayproject'
|
||||
password: 'openrelayproject'
|
||||
openrelay3:
|
||||
url: 'turn:openrelay.metered.ca:443?transport=tcp'
|
||||
username: 'openrelayproject'
|
||||
password: 'openrelayproject'
|
@ -0,0 +1,63 @@
|
||||
listener_01:
|
||||
address: 0.0.0.0:8081
|
||||
address_v6: 'null'
|
||||
max_players: 60
|
||||
forward_ip: false
|
||||
forward_ip_header: X-Real-IP
|
||||
redirect_legacy_clients_to: 'null'
|
||||
server_icon: server-icon.png
|
||||
server_motd:
|
||||
- '&6An EaglercraftX server'
|
||||
allow_motd: true
|
||||
allow_query: true
|
||||
request_motd_cache:
|
||||
cache_ttl: 7200
|
||||
online_server_list_animation: false
|
||||
online_server_list_results: true
|
||||
online_server_list_trending: true
|
||||
online_server_list_portfolios: false
|
||||
http_server:
|
||||
enabled: false
|
||||
root: 'web'
|
||||
page_404_not_found: 'default'
|
||||
page_index_name:
|
||||
- 'index.html'
|
||||
- 'index.htm'
|
||||
allow_voice: false
|
||||
ratelimit:
|
||||
ip:
|
||||
enable: true
|
||||
period: 90
|
||||
limit: 60
|
||||
limit_lockout: 80
|
||||
lockout_duration: 1200
|
||||
exceptions:
|
||||
- '127.*'
|
||||
- '0:0:0:0:0:0:0:1'
|
||||
login:
|
||||
enable: true
|
||||
period: 50
|
||||
limit: 5
|
||||
limit_lockout: 10
|
||||
lockout_duration: 300
|
||||
exceptions:
|
||||
- '127.*'
|
||||
- '0:0:0:0:0:0:0:1'
|
||||
motd:
|
||||
enable: true
|
||||
period: 30
|
||||
limit: 5
|
||||
limit_lockout: 15
|
||||
lockout_duration: 300
|
||||
exceptions:
|
||||
- '127.*'
|
||||
- '0:0:0:0:0:0:0:1'
|
||||
query:
|
||||
enable: true
|
||||
period: 30
|
||||
limit: 15
|
||||
limit_lockout: 25
|
||||
lockout_duration: 900
|
||||
exceptions:
|
||||
- '127.*'
|
||||
- '0:0:0:0:0:0:0:1'
|
@ -0,0 +1,25 @@
|
||||
server_name: 'EaglercraftXVelocity Server'
|
||||
server_uuid: ${random_uuid}
|
||||
websocket_connection_timeout: 15000
|
||||
websocket_handshake_timeout: 5000
|
||||
http_websocket_compression_level: 6
|
||||
download_vanilla_skins_to_clients: true
|
||||
valid_skin_download_urls:
|
||||
- 'textures.minecraft.net'
|
||||
uuid_lookup_ratelimit_player: 50
|
||||
uuid_lookup_ratelimit_global: 175
|
||||
skin_download_ratelimit_player: 1000
|
||||
skin_download_ratelimit_global: 30000
|
||||
skin_cache_db_uri: 'jdbc:sqlite:eaglercraft_skins_cache.db'
|
||||
skin_cache_keep_objects_days: 45
|
||||
skin_cache_keep_profiles_days: 7
|
||||
skin_cache_max_objects: 32768
|
||||
skin_cache_max_profiles: 32768
|
||||
skin_cache_antagonists_ratelimit: 15
|
||||
sql_driver_class: 'internal'
|
||||
sql_driver_path: 'internal'
|
||||
eagler_players_vanilla_skin: ''
|
||||
enable_is_eagler_player_property: true
|
||||
disable_voice_chat_on_servers: []
|
||||
disable_fnaw_skins_everywhere: false
|
||||
disable_fnaw_skins_on_servers: []
|
@ -0,0 +1,9 @@
|
||||
block_all_client_updates: false
|
||||
discard_login_packet_certs: false
|
||||
cert_packet_data_rate_limit: 524288
|
||||
enable_eagcert_folder: true
|
||||
download_latest_certs: true
|
||||
download_certs_from:
|
||||
- 'https://eaglercraft.com/backup.cert'
|
||||
- 'https://deev.is/eagler/backup.cert'
|
||||
check_for_update_every: 900
|
@ -0,0 +1 @@
|
||||
{"id":"eaglerxvelocity","name":"EaglercraftXVelocity","version":"1.0.0","description":"Plugin to allow EaglercraftX 1.8 players to join your network, or allow EaglercraftX 1.8 players to use your network as a proxy to join other networks","authors":["lax1dude", "ayunami2000"],"dependencies":[],"main":"net.lax1dude.eaglercraft.v1_8.plugin.gateway_velocity.EaglerXVelocity"}
|
Reference in New Issue
Block a user