<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:
lax1dude 2024-05-25 18:58:41 -07:00
parent 9bb6f6d4ec
commit 40a60f992e
89 changed files with 14610 additions and 1 deletions

42
CREDITS
View File

@ -11,6 +11,7 @@
- Made the integrated PBR resource pack
- Wrote all desktop emulation code
- Wrote EaglercraftXBungee
- Wrote EaglercraftXVelocity
- Wrote WebRTC relay server
- Wrote voice chat server
- Wrote the patch and build system
@ -20,6 +21,7 @@
- Many bug fixes
- WebRTC LAN worlds
- WebRTC voice chat
- Made velocity plugin work
- Added resource packs
- Added screen recording
- Added seamless fullscreen
@ -584,6 +586,44 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Project Name: BungeeCord
Project Author: md_5
Project URL: https://www.spigotmc.org/go/bungeecord/
Used For: parsing YAML config files in EaglercraftXVelocity
* 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.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Project Name: 3D Sound System
Project Author: Paul Lamb
Project URL: http://www.paulscode.com/forum/index.php?topic=4.0
@ -706,6 +746,6 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Make sure you also update the copy of this file in "sources/resources/assets/eagler/CREDITS"
Make sure you also update the copy of this file in "sources/resources/assets/eagler/CREDITS.txt"
The content of both files should match, but not include this notice in the resources one

View File

@ -68,6 +68,8 @@ EaglercraftX 1.8 includes an integrated voice-chat service that can be used in s
To make a server for EaglercraftX 1.8 the recommended software to use is EaglercraftXBungee ("EaglerXBungee") which is included in this repository in the `gateway/EaglercraftXBungee` folder. This is a plugin designed to be used with BungeeCord to allow Eaglercraft players to join your BungeeCord server. It is assumed that the reader already knows what BungeeCord is and has a working server set up that is joinable via java edition. If you don't know what BungeeCord is, please research the topic yourself first before continuing. Waterfall and FlameCord have also been tested, but EaglerXBungee was natively compiled against BungeeCord.
There is an experimental velocity plugin available in `gateway/EaglercraftXVelocity` but it is still in development and not recommended for public servers, so be sure to check for updates regularly if you use it. Configuration files are basically identical to EaglercraftXBungee so its safe to just directy copy in your old EaglercraftXBungee config files to the `plugins/eaglerxvelocity` folder and they should work with a minimal number of edits if you are migrating your network from BungeeCord to Velocity.
### Installation
Obtain the latest version of the EaglerXBungee JAR file (it can be downloaded in the client from the "Multiplayer" screen) and place it in the "plugins" folder of your BungeeCord server. It's recommended to only join native Minecraft 1.8 servers through an EaglerXBungee server but plugins like ProtocolSupport have allowed some people to join newer servers too.

View File

@ -0,0 +1,4 @@
lib/*
.idea/*
*.iml
out/*

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("<", "&lt;").replace(">", "&gt;") + "</span>";
}
public void channelInactive(ChannelHandlerContext ctx) {
EaglerPipeline.closeChannel(ctx.channel());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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("<", "&lt;").replace(">", "&gt;");
}
}

View File

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

View File

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

View File

@ -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 {
}
}

View File

@ -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
* &gt; 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 &lt;= 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 &lt;= 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 &lt;= 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 &lt;= 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.");
}
}
}

View File

@ -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 &gt; 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} &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: []

View File

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

View File

@ -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"}

1
gateway_version_velocity Normal file
View File

@ -0,0 +1 @@
1.0.0

View File

@ -11,6 +11,7 @@
- Made the integrated PBR resource pack
- Wrote all desktop emulation code
- Wrote EaglercraftXBungee
- Wrote EaglercraftXVelocity
- Wrote WebRTC relay server
- Wrote voice chat server
- Wrote the patch and build system
@ -20,6 +21,7 @@
- Many bug fixes
- WebRTC LAN worlds
- WebRTC voice chat
- Made velocity plugin work
- Added resource packs
- Added screen recording
- Added seamless fullscreen
@ -584,6 +586,44 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Project Name: BungeeCord
Project Author: md_5
Project URL: https://www.spigotmc.org/go/bungeecord/
Used For: parsing YAML config files in EaglercraftXVelocity
* 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.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Project Name: 3D Sound System
Project Author: Paul Lamb
Project URL: http://www.paulscode.com/forum/index.php?topic=4.0