commit 06f03a918c79d66e242552ac8619961fd33608f9 Author: HoosierTransfer Date: Thu May 9 07:27:22 2024 -0400 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..512a13e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +lib/* +.idea/* +*.iml +out/* \ No newline at end of file diff --git a/BungeeCord.jar b/BungeeCord.jar new file mode 100644 index 0000000..6ab36a1 Binary files /dev/null and b/BungeeCord.jar differ diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..73dece3 --- /dev/null +++ b/readme.txt @@ -0,0 +1,9 @@ +Plugin for eaglercraft on bungeecord + +Not using gradle to give more direct access to bungeecord's internals, as gradle only provides a dummy jar containing the api. + +EaglercraftXBungee requires netty's websocket client/server, which is already in the production bungeecord 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 bungeecord 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 bungeecord jar \ No newline at end of file diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF new file mode 100644 index 0000000..98420d2 --- /dev/null +++ b/src/main/java/META-INF/MANIFEST.MF @@ -0,0 +1,4 @@ +Manifest-Version: 1.0 +Main-Class: net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.shit + .MainClass + diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/EaglerXBungee.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/EaglerXBungee.java new file mode 100644 index 0000000..a286ca1 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/EaglerXBungee.java @@ -0,0 +1,321 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Timer; +import java.util.TimerTask; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.auth.DefaultAuthSystem; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command.CommandConfirmCode; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command.CommandDomain; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command.CommandEaglerPurge; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command.CommandEaglerRegister; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command.CommandRatelimit; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerAuthConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerBungeeConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.handlers.EaglerPacketEventListener; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.handlers.EaglerPluginEventListener; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerPipeline; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.web.HttpWebServer; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.shit.CompatWarning; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.BinaryHttpClient; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.CapeServiceOffline; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.ISkinService; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.SkinService; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.SkinServiceOffline; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.voice.VoiceService; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.plugin.PluginManager; +import net.md_5.bungee.netty.PipelineUtils; +import net.md_5.bungee.BungeeCord; + +/** + * 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 EaglerXBungee extends Plugin { + + public static final String NATIVE_BUNGEECORD_BUILD = "1.20-R0.3-SNAPSHOT:67c65e0:1828"; + public static final String NATIVE_WATERFALL_BUILD = "1.20-R0.3-SNAPSHOT:da6aaf6:572"; + + static { + CompatWarning.displayCompatWarning(); + } + + private static EaglerXBungee instance = null; + private EaglerBungeeConfig conf = null; + private EventLoopGroup eventLoopGroup; + private Collection openChannels; + private final 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; + + public EaglerXBungee() { + instance = this; + openChannels = new LinkedList(); + closeInactiveConnections = new Timer("EaglerXBungee: Network Tick Tasks"); + newChannelListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture ch) throws Exception { + synchronized(openChannels) { // synchronize whole block to preserve logging order + if(ch.isSuccess()) { + EaglerXBungee.logger().info("Eaglercraft is listening on: " + ch.channel().attr(EaglerPipeline.LOCAL_ADDRESS).get().toString()); + openChannels.add(ch.channel()); + }else { + EaglerXBungee.logger().severe("Eaglercraft could not bind port: " + ch.channel().attr(EaglerPipeline.LOCAL_ADDRESS).get().toString()); + EaglerXBungee.logger().severe("Reason: " + ch.cause().toString()); + } + } + } + }; + } + + @Override + public void onLoad() { + try { + eventLoopGroup = ((BungeeCord) getProxy()).eventLoops; + } catch (NoSuchFieldError e) { + try { + eventLoopGroup = (EventLoopGroup) BungeeCord.class.getField("workerEventLoopGroup").get(getProxy()); + } catch (IllegalAccessException | NoSuchFieldException ex) { + throw new RuntimeException(ex); + } + } + reloadConfig(); + closeInactiveConnections.scheduleAtFixedRate(EaglerPipeline.closeInactive, 0l, 250l); + } + + @Override + public void onEnable() { + PluginManager mgr = getProxy().getPluginManager(); + mgr.registerListener(this, new EaglerPluginEventListener(this)); + mgr.registerListener(this, new EaglerPacketEventListener(this)); + mgr.registerCommand(this, new CommandRatelimit()); + mgr.registerCommand(this, new CommandConfirmCode()); + mgr.registerCommand(this, new CommandDomain()); + EaglerAuthConfig authConf = conf.getAuthConfig(); + if(authConf.isEnableAuthentication() && authConf.isUseBuiltInAuthentication()) { + if(!BungeeCord.getInstance().getConfig().isOnlineMode()) { + getLogger().severe("Online mode is set to false! Authentication system has been disabled"); + authConf.triggerOnlineModeDisabled(); + }else { + mgr.registerCommand(this, new CommandEaglerRegister(authConf.getEaglerCommandName())); + mgr.registerCommand(this, new CommandEaglerPurge(authConf.getEaglerCommandName())); + } + } + getProxy().registerChannel(SkinService.CHANNEL); + getProxy().registerChannel(CapeServiceOffline.CHANNEL); + getProxy().registerChannel(EaglerPipeline.UPDATE_CERT_CHANNEL); + getProxy().registerChannel(VoiceService.CHANNEL); + getProxy().registerChannel(EaglerPacketEventListener.FNAW_SKIN_ENABLE_CHANNEL); + 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("EaglerXBungee: Skin Service Tasks"); + skinServiceTasks.schedule(new TimerTask() { + @Override + public void run() { + try { + skinService.flush(); + }catch(Throwable t) { + logger().log(Level.SEVERE, "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().log(Level.SEVERE, "Could not load authentication system!", ex); + } + if(defaultAuthSystem != null) { + authServiceTasks = new Timer("EaglerXBungee: Auth Service Tasks"); + authServiceTasks.schedule(new TimerTask() { + @Override + public void run() { + try { + defaultAuthSystem.flush(); + }catch(Throwable t) { + logger().log(Level.SEVERE, "Error flushing auth cache!", t); + } + } + }, 60000l, 60000l); + } + } + if(conf.getEnableVoiceChat()) { + voiceService = new VoiceService(conf); + logger().warning("Voice chat enabled, not recommended for public servers!"); + }else { + logger().info("Voice chat disabled, add \"allow_voice: true\" to your listeners to enable"); + } + } + + @Override + public void onDisable() { + PluginManager mgr = getProxy().getPluginManager(); + mgr.unregisterListeners(this); + mgr.unregisterCommands(this); + getProxy().unregisterChannel(SkinService.CHANNEL); + getProxy().unregisterChannel(CapeServiceOffline.CHANNEL); + getProxy().unregisterChannel(EaglerPipeline.UPDATE_CERT_CHANNEL); + getProxy().unregisterChannel(VoiceService.CHANNEL); + getProxy().unregisterChannel(EaglerPacketEventListener.FNAW_SKIN_ENABLE_CHANNEL); + stopListeners(); + 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 = EaglerBungeeConfig.loadConfig(getDataFolder()); + 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) + .channel(PipelineUtils.getServerChannel(addr)) + .group(eventLoopGroup) + .childAttr(EaglerPipeline.LISTENER, confData) + .attr(EaglerPipeline.LOCAL_ADDRESS, addr) + .localAddress(addr) + .childHandler(EaglerPipeline.SERVER_CHILD) + .bind().addListener(newChannelListener); + } + + public void stopListeners() { + synchronized(openChannels) { + for(Channel c : openChannels) { + c.close().syncUninterruptibly(); + EaglerXBungee.logger().info("Eaglercraft listener closed: " + c.attr(EaglerPipeline.LOCAL_ADDRESS).get().toString()); + } + openChannels.clear(); + } + synchronized(EaglerPipeline.openChannels) { + EaglerPipeline.openChannels.clear(); + } + } + + public EaglerBungeeConfig getConfig() { + return conf; + } + + public EventLoopGroup getEventLoopGroup() { + return eventLoopGroup; + } + + public ISkinService getSkinService() { + return skinService; + } + + public CapeServiceOffline getCapeService() { + return capeService; + } + + public DefaultAuthSystem getAuthService() { + return defaultAuthSystem; + } + + public VoiceService getVoiceService() { + return voiceService; + } + + public static EaglerXBungee getEagler() { + return instance; + } + + public static Logger logger() { + return instance.getLogger(); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftHandleAuthPasswordEvent.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftHandleAuthPasswordEvent.java new file mode 100644 index 0000000..49bdf58 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftHandleAuthPasswordEvent.java @@ -0,0 +1,196 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event; + +import java.net.InetAddress; +import java.util.UUID; +import java.util.function.Consumer; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.md_5.bungee.api.plugin.Event; + +/** + * 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 extends Event { + + 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 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 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 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; + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftIsAuthRequiredEvent.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftIsAuthRequiredEvent.java new file mode 100644 index 0000000..74d4c1e --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftIsAuthRequiredEvent.java @@ -0,0 +1,158 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event; + +import java.net.InetAddress; +import java.util.function.Consumer; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.md_5.bungee.api.plugin.Event; + +/** + * 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 extends Event { + + 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 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 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 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); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftMOTDEvent.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftMOTDEvent.java new file mode 100644 index 0000000..5c5396c --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftMOTDEvent.java @@ -0,0 +1,48 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event; + +import java.net.InetAddress; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.query.MOTDConnection; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.md_5.bungee.api.plugin.Event; + +/** + * 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 extends Event { + + 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftRegisterSkinEvent.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftRegisterSkinEvent.java new file mode 100644 index 0000000..80159db --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/event/EaglercraftRegisterSkinEvent.java @@ -0,0 +1,118 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event; + +import java.util.UUID; + +import net.md_5.bungee.api.plugin.Event; +import net.md_5.bungee.protocol.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 extends Event { + + private final String username; + private final UUID uuid; + private Property useMojangProfileProperty = null; + private boolean useLoginResultTextures = false; + private int presetId = -1; + 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; + presetId = -1; + customTex = null; + customURL = null; + } + + public void setForceUseLoginResultObjectTextures(boolean b) { + useMojangProfileProperty = null; + useLoginResultTextures = b; + presetId = -1; + customTex = null; + customURL = null; + } + + public void setForceUsePreset(int p) { + useMojangProfileProperty = null; + useLoginResultTextures = false; + presetId = 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); + customURL = null; + } + + public void setForceUseCustom(int model, byte[] tex) { + useMojangProfileProperty = null; + useLoginResultTextures = false; + presetId = -1; + 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; + presetId = -1; + customTex = packet; + customURL = null; + } + + public void setForceUseURL(String url) { + useMojangProfileProperty = null; + useLoginResultTextures = false; + presetId = -1; + 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/EaglerQueryHandler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/EaglerQueryHandler.java new file mode 100644 index 0000000..90f2480 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/EaglerQueryHandler.java @@ -0,0 +1,31 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.query; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.HttpServerQueryHandler; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 clazz) { + QueryManager.registerQueryType(name, clazz); + } + + public static void unregisterQueryType(String name) { + QueryManager.unregisterQueryType(name); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/EaglerQuerySimpleHandler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/EaglerQuerySimpleHandler.java new file mode 100644 index 0000000..0d1f526 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/EaglerQuerySimpleHandler.java @@ -0,0 +1,61 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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() { + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/MOTDConnection.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/MOTDConnection.java new file mode 100644 index 0000000..f6a75f8 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/api/query/MOTDConnection.java @@ -0,0 +1,55 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.query; + +import java.net.InetAddress; +import java.util.List; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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(); + + String getLine1(); + String getLine2(); + List getPlayerList(); + int[] getBitmap(); + int getOnlinePlayers(); + int getMaxPlayers(); + String getSubType(); + + void setLine1(String p); + void setLine2(String p); + void setPlayerList(List p); + void setPlayerList(String... p); + void setBitmap(int[] p); + void setOnlinePlayers(int i); + void setMaxPlayers(int i); + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/AuthLoadingCache.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/AuthLoadingCache.java new file mode 100644 index 0000000..db1a352 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/AuthLoadingCache.java @@ -0,0 +1,114 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 { + + private static class CacheEntry { + + private long lastHit; + private V instance; + + private CacheEntry(V instance) { + this.lastHit = System.currentTimeMillis(); + this.instance = instance; + } + + } + + public static interface CacheLoader { + V load(K key); + } + + public static interface CacheVisitor { + boolean shouldEvict(K key, V value); + } + + private final Map> cacheMap; + private final CacheLoader provider; + private final long cacheTTL; + + private long cacheTimer; + + public AuthLoadingCache(CacheLoader provider, long cacheTTL) { + this.cacheMap = new HashMap(); + this.provider = provider; + this.cacheTTL = cacheTTL; + } + + public V get(K key) { + CacheEntry 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 visitor) { + synchronized(cacheMap) { + Iterator>> itr = cacheMap.entrySet().iterator(); + while(itr.hasNext()) { + Entry> 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> mapItr = cacheMap.values().iterator(); + while(mapItr.hasNext()) { + CacheEntry etr = mapItr.next(); + if(millis - etr.lastHit > cacheTTL) { + mapItr.remove(); + } + } + } + } + } + + public void flush() { + synchronized(cacheMap) { + cacheMap.clear(); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/DefaultAuthSystem.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/DefaultAuthSystem.java new file mode 100644 index 0000000..80920e5 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/DefaultAuthSystem.java @@ -0,0 +1,680 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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.Properties; +import java.util.Random; +import java.util.UUID; +import java.util.logging.Level; + +import org.apache.commons.codec.binary.Base64; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftHandleAuthPasswordEvent; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftIsAuthRequiredEvent; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftIsAuthRequiredEvent.AuthMethod; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftIsAuthRequiredEvent.AuthResponse; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerAuthConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.sqlite.EaglerDrivers; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.PendingConnection; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.connection.InitialHandler; +import net.md_5.bungee.connection.LoginResult; +import net.md_5.bungee.protocol.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 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); + } + EaglerXBungee.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 { + + @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 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); + EaglerXBungee.logger().warning("User \"" + info.mojangUsername + "\" entered the wrong password while logging in from: " + 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) { + EaglerXBungee.logger().log(Level.SEVERE, "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(ProxiedPlayer player, String password) throws TooManyRegisteredOnIPException, AuthException { + PendingConnection conn = player.getPendingConnection(); + if(conn instanceof EaglerInitialHandler) { + throw new AuthException("Cannot register from an eaglercraft account!"); + }else if(!conn.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.getName(); + 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.getSocketAddress()); + 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); + LoginResult res = ((InitialHandler)player.getPendingConnection()).getLoginProfile(); + 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(LoginResult profile) { + try { + Property[] props = profile.getProperties(); + for(int i = 0; i < props.length; ++i) { + Property prop = props[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) { + ProxiedPlayer player = loginEvent.getPlayer(); + PendingConnection con = player.getPendingConnection(); + if(!(con instanceof EaglerInitialHandler)) { + 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.getName(); + if(!playerName.equals(username)) { + EaglerXBungee.logger().info("Player \"" + uuid.toString() + "\" changed their username from \"" + username + + " to \"" + playerName + "\", updating authentication database..."); + 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(((InitialHandler)con).getLoginProfile()); + if(texProperty != null) { + synchronized(updateTextures) { + updateTextures.setBytes(1, texProperty); + updateTextures.setString(2, uuidString); + updateTextures.executeUpdate(); + } + } + }catch(SQLException ex) { + EaglerXBungee.logger().log(Level.SEVERE, "Could not look up UUID \"" + uuid.toString() + "\" in auth database!", 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); + } + TextComponent comp = new TextComponent(lastEaglerLoginMessage.replace("$date", dateStr).replace("$ip", "" + lastLoginIP)); + comp.setColor(ChatColor.GREEN); + player.sendMessage(comp); + } + player.sendMessage(new TextComponent(useChangeCommandText)); + }else { + player.sendMessage(new TextComponent(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(); + EaglerXBungee.logger().info("Successfully disconnected from database '" + uri + "'"); + } catch (SQLException e) { + EaglerXBungee.logger().log(Level.WARNING, "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"; + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/GeneralDigest.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/GeneralDigest.java new file mode 100644 index 0000000..9283c4a --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/GeneralDigest.java @@ -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_bungeecord.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(); +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/HashUtils.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/HashUtils.java new file mode 100644 index 0000000..7337e51 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/HashUtils.java @@ -0,0 +1,32 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 }; + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/SHA1Digest.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/SHA1Digest.java new file mode 100644 index 0000000..0bfbb9a --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/SHA1Digest.java @@ -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_bungeecord.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); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/SHA256Digest.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/SHA256Digest.java new file mode 100644 index 0000000..c57a935 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/auth/SHA256Digest.java @@ -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_bungeecord.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 }; + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandConfirmCode.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandConfirmCode.java new file mode 100644 index 0000000..c996741 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandConfirmCode.java @@ -0,0 +1,50 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command; + +import java.nio.charset.StandardCharsets; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.auth.SHA1Digest; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.plugin.Command; + +/** + * 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 CommandConfirmCode extends Command { + + public static String confirmHash = null; + + public CommandConfirmCode() { + super("confirm-code", "eaglercraft.command.confirmcode", "confirmcode"); + } + + @Override + public void execute(CommandSender var1, String[] var2) { + if(var2.length != 1) { + var1.sendMessage(new TextComponent(ChatColor.RED + "How to use: " + ChatColor.WHITE + "/confirm-code ")); + }else { + var1.sendMessage(new TextComponent(ChatColor.YELLOW + "Server list 2FA code has been set to: " + ChatColor.GREEN + var2[0])); + var1.sendMessage(new TextComponent(ChatColor.YELLOW + "You can now return to the server list site and continue")); + 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); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandDomain.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandDomain.java new file mode 100644 index 0000000..8185b5d --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandDomain.java @@ -0,0 +1,57 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.PendingConnection; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; + +/** + * 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 CommandDomain extends Command { + + public CommandDomain() { + super("domain", "eaglercraft.command.domain"); + } + + @Override + public void execute(CommandSender var1, String[] var2) { + if(var2.length != 1) { + var1.sendMessage(new TextComponent(ChatColor.RED + "How to use: " + ChatColor.WHITE + "/domain ")); + }else { + ProxiedPlayer player = ProxyServer.getInstance().getPlayer(var2[0]); + if(player == null) { + var1.sendMessage(new TextComponent(ChatColor.RED + "That user is not online")); + return; + } + PendingConnection conn = player.getPendingConnection(); + if(!(conn instanceof EaglerInitialHandler)) { + var1.sendMessage(new TextComponent(ChatColor.RED + "That user is not using Eaglercraft")); + return; + } + String origin = ((EaglerInitialHandler)conn).origin; + if(origin != null) { + var1.sendMessage(new TextComponent(ChatColor.BLUE + "Domain of " + var2[0] + " is '" + origin + "'")); + }else { + var1.sendMessage(new TextComponent(ChatColor.RED + "That user's browser did not send an origin header")); + } + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandEaglerPurge.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandEaglerPurge.java new file mode 100644 index 0000000..9942f1c --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandEaglerPurge.java @@ -0,0 +1,82 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command; + +import java.util.logging.Level; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.auth.DefaultAuthSystem; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.auth.DefaultAuthSystem.AuthException; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerAuthConfig; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.command.ConsoleCommandSender; + +/** + * 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 CommandEaglerPurge extends Command { + + public CommandEaglerPurge(String name) { + super(name + "-purge", "eaglercraft.command.purge"); + } + + @Override + public void execute(CommandSender var1, String[] var2) { + if(var1 instanceof ConsoleCommandSender) { + if(var2.length != 1) { + TextComponent comp = new TextComponent("Use /" + getName() + " "); + comp.setColor(ChatColor.RED); + var1.sendMessage(comp); + return; + } + int mx; + try { + mx = Integer.parseInt(var2[0]); + }catch(NumberFormatException ex) { + TextComponent comp = new TextComponent("'" + var2[0] + "' is not an integer!"); + comp.setColor(ChatColor.RED); + var1.sendMessage(comp); + return; + } + EaglerAuthConfig authConf = EaglerXBungee.getEagler().getConfig().getAuthConfig(); + if(authConf.isEnableAuthentication() && authConf.isUseBuiltInAuthentication()) { + DefaultAuthSystem srv = EaglerXBungee.getEagler().getAuthService(); + if(srv != null) { + int cnt; + try { + EaglerXBungee.logger().warning("Console is attempting to purge all accounts with " + mx + " days of inactivity"); + cnt = srv.pruneUsers(System.currentTimeMillis() - mx * 86400000l); + }catch(AuthException ex) { + EaglerXBungee.logger().log(Level.SEVERE, "Failed to purge accounts", ex); + TextComponent comp = new TextComponent("Failed to purge, check log! Reason: " + ex.getMessage()); + comp.setColor(ChatColor.AQUA); + var1.sendMessage(comp); + return; + } + EaglerXBungee.logger().warning("Console purged " + cnt + " accounts from auth database"); + TextComponent comp = new TextComponent("Purged " + cnt + " old accounts from the database"); + comp.setColor(ChatColor.AQUA); + var1.sendMessage(comp); + } + } + }else { + TextComponent comp = new TextComponent("This command can only be run from the console!"); + comp.setColor(ChatColor.RED); + var1.sendMessage(comp); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandEaglerRegister.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandEaglerRegister.java new file mode 100644 index 0000000..2c6a13a --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandEaglerRegister.java @@ -0,0 +1,75 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command; + +import java.util.logging.Level; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.auth.DefaultAuthSystem; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerAuthConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.plugin.Command; + +/** + * 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 CommandEaglerRegister extends Command { + + public CommandEaglerRegister(String name) { + super(name); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if(sender instanceof ProxiedPlayer) { + ProxiedPlayer player = (ProxiedPlayer)sender; + if(args.length != 1) { + TextComponent comp = new TextComponent("Use: /" + getName() + " "); + comp.setColor(ChatColor.RED); + player.sendMessage(comp); + return; + } + EaglerAuthConfig authConf = EaglerXBungee.getEagler().getConfig().getAuthConfig(); + if(authConf.isEnableAuthentication() && authConf.isUseBuiltInAuthentication()) { + DefaultAuthSystem srv = EaglerXBungee.getEagler().getAuthService(); + if(srv != null) { + if(!(player.getPendingConnection() instanceof EaglerInitialHandler)) { + try { + srv.processSetPassword(player, args[0]); + sender.sendMessage(new TextComponent(authConf.getCommandSuccessText())); + }catch(DefaultAuthSystem.TooManyRegisteredOnIPException ex) { + String tooManyReg = authConf.getTooManyRegistrationsMessage(); + sender.sendMessage(new TextComponent(tooManyReg)); + }catch(DefaultAuthSystem.AuthException ex) { + EaglerXBungee.logger().log(Level.SEVERE, "Internal exception while processing password change from \"" + player.getName() + "\"", ex); + TextComponent comp = new TextComponent("Internal error, check console logs"); + comp.setColor(ChatColor.RED); + sender.sendMessage(comp); + } + }else { + player.sendMessage(new TextComponent(authConf.getNeedVanillaToRegisterMessage())); + } + } + } + }else { + TextComponent comp = new TextComponent("You must be a player to use this command!"); + comp.setColor(ChatColor.RED); + sender.sendMessage(comp); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandRatelimit.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandRatelimit.java new file mode 100644 index 0000000..29e8a77 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/command/CommandRatelimit.java @@ -0,0 +1,88 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerBungeeConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerRateLimiter; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.CommandSender; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.plugin.Command; + +/** + * 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 CommandRatelimit extends Command { + + public CommandRatelimit() { + super("ratelimit", "eaglercraft.command.ratelimit"); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if((args.length != 1 && args.length != 2) || !args[0].equalsIgnoreCase("reset")) { + TextComponent comp = new TextComponent("Usage: /ratelimit reset [ip|login|motd|query]"); //TODO: allow reset ratelimit on specific listeners + comp.setColor(ChatColor.RED); + sender.sendMessage(comp); + }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 { + TextComponent comp = new TextComponent("Unknown ratelimit '" + args[1] + "'!"); + comp.setColor(ChatColor.RED); + sender.sendMessage(comp); + return; + } + } + EaglerBungeeConfig conf = EaglerXBungee.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(new TextComponent("Ratelimits reset.")); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerAuthConfig.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerAuthConfig.java new file mode 100644 index 0000000..6a3be28 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerAuthConfig.java @@ -0,0 +1,165 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.config.Configuration; + +/** + * 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 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerBungeeConfig.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerBungeeConfig.java new file mode 100644 index 0000000..b4cf887 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerBungeeConfig.java @@ -0,0 +1,478 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.web.HttpContentType; +import net.md_5.bungee.config.Configuration; +import net.md_5.bungee.config.ConfigurationProvider; +import net.md_5.bungee.config.YamlConfiguration; +import net.md_5.bungee.protocol.Property; + +/** + * 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 EaglerBungeeConfig { + + public static EaglerBungeeConfig loadConfig(File directory) throws IOException { + Map contentTypes = new HashMap(); + + try(InputStream is = new FileInputStream(getConfigFile(directory, "http_mime_types.json"))) { + loadMimeTypes(is, contentTypes); + }catch(Throwable t) { + try(InputStream is = EaglerBungeeConfig.class.getResourceAsStream("default_http_mime_types.json")) { + loadMimeTypes(is, contentTypes); + }catch(IOException ex) { + EaglerXBungee.logger().severe("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", "EaglercraftXBungee 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 listeners = listenerYml.getKeys().iterator(); + Map 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 { + EaglerXBungee.logger().severe("Invalid listener config: " + name); + } + } + + if(serverListeners.size() == 0) { + EaglerXBungee.logger().severe("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 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 validSkinUrls = (Collection)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 disableVoiceOnServers = new HashSet((Collection)configYml.getList("disable_voice_chat_on_servers")); + boolean disableFNAWSkinsEverywhere = configYml.getBoolean("disable_fnaw_skins_everywhere", false); + Set disableFNAWSkinsOnServers = new HashSet((Collection)configYml.getList("disable_fnaw_skins_on_servers")); + + final EaglerBungeeConfig ret = new EaglerBungeeConfig(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( + EaglerBungeeConfig.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 contentTypes) throws IOException { + JsonObject obj = parseJsonObject(file); + for(Entry etr : obj.entrySet()) { + String mime = etr.getKey(); + try { + JsonObject e = etr.getValue().getAsJsonObject(); + JsonArray arr = e.getAsJsonArray("files"); + if(arr == null || arr.size() == 0) { + EaglerXBungee.logger().warning("MIME type '" + mime + "' defines no extensions!"); + continue; + } + HashSet 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) { + EaglerXBungee.logger().warning("Exception parsing MIME type '" + mime + "' - " + t.toString()); + } + } + } + + private static Collection loadICEServers(Configuration config) { + Collection ret = new ArrayList(config.getList("voice_stun_servers")); + Configuration turnServers = config.getSection("voice_turn_servers"); + Iterator 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 (new JsonParser()).parse(str.toString()).getAsJsonObject(); + }catch(JsonSyntaxException ex) { + throw new IOException("Invalid JSONObject", ex); + } + } + + public static final Property isEaglerProperty = new Property("isEaglerPlayer", "true"); + + private final String serverName; + private final UUID serverUUID; + private final long websocketKeepAliveTimeout; + private final long websocketHandshakeTimeout; + private final int httpWebsocketCompressionLevel; + private final Map serverListeners; + private final Map contentTypes; + private final boolean downloadVanillaSkins; + private final Collection 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 iceServers; + private final boolean enableVoiceChat; + private final Set disableVoiceOnServers; + private final boolean disableFNAWSkinsEverywhere; + private final Set 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 getServerListeners() { + return serverListeners.values(); + } + + public EaglerListenerConfig getServerListenersByName(String name) { + return serverListeners.get(name); + } + + public Map getContentType() { + return contentTypes; + } + + public Map getContentTypes() { + return contentTypes; + } + + public boolean getDownloadVanillaSkins() { + return downloadVanillaSkins; + } + + public Collection 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 getICEServers() { + return iceServers; + } + + public boolean getEnableVoiceChat() { + return enableVoiceChat; + } + + public Set getDisableVoiceOnServersSet() { + return disableVoiceOnServers; + } + + public boolean getDisableFNAWSkinsEverywhere() { + return disableFNAWSkinsEverywhere; + } + + public Set getDisableFNAWSkinsOnServersSet() { + return disableFNAWSkinsOnServers; + } + + private EaglerBungeeConfig(String serverName, UUID serverUUID, long websocketKeepAliveTimeout, + long websocketHandshakeTimeout, int httpWebsocketCompressionLevel, + Map serverListeners, Map contentTypes, + boolean downloadVanillaSkins, Collection 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 iceServers, boolean enableVoiceChat, Set disableVoiceOnServers, + boolean disableFNAWSkinsEverywhere, Set 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerListenerConfig.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerListenerConfig.java new file mode 100644 index 0000000..de2e56e --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerListenerConfig.java @@ -0,0 +1,312 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config; + +import java.io.File; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.netty.handler.codec.http.HttpRequest; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.web.HttpContentType; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.web.HttpWebServer; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.config.Configuration; + +/** + * 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 EaglerListenerConfig extends ListenerInfo { + + static EaglerListenerConfig loadConfig(Configuration config, Map 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); + String tabListType = config.getString("tab_list", "GLOBAL_PING"); + String defaultServer = config.getString("default_server", "lobby"); + boolean forceDefaultServer = config.getBoolean("force_default_server", false); + 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 serverMOTD = (List) 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 defaultIndex = Arrays.asList("index.html", "index.htm"); + List indexPageRaw = httpServerConf.getList("page_index_name", defaultIndex); + List 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(EaglerXBungee.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, tabListType, defaultServer, forceDefaultServer, + 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 String tabListType; + private final String defaultServer; + private final boolean forceDefaultServer; + private final boolean forwardIp; + private final String forwardIpHeader; + private final String redirectLegacyClientsTo; + private final String serverIcon; + private final List 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, + String tabListType, String defaultServer, boolean forceDefaultServer, boolean forwardIp, + String forwardIpHeader, String redirectLegacyClientsTo, String serverIcon, List serverMOTD, + boolean allowMOTD, boolean allowQuery, MOTDCacheConfiguration motdCacheConfig, HttpWebServer webServer, + boolean enableVoiceChat, EaglerRateLimiter ratelimitIp, EaglerRateLimiter ratelimitLogin, + EaglerRateLimiter ratelimitMOTD, EaglerRateLimiter ratelimitQuery) { + super(address, String.join("\n", serverMOTD), maxPlayer, 60, Arrays.asList(defaultServer), forceDefaultServer, + Collections.emptyMap(), tabListType, false, false, 0, false, false); + this.address = address; + this.addressV6 = addressV6; + this.maxPlayer = maxPlayer; + this.tabListType = tabListType; + this.defaultServer = defaultServer; + this.forceDefaultServer = forceDefaultServer; + 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 String getTabListType() { + return tabListType; + } + + public String getDefaultServer() { + return defaultServer; + } + + public boolean isForceDefaultServer() { + return forceDefaultServer; + } + + 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) { + EaglerXBungee.logger().warning("Server icon could not be loaded: " + f.getAbsolutePath()); + } + }else { + EaglerXBungee.logger().warning("Server icon is not a file: " + f.getAbsolutePath()); + } + } + serverIconSet = true; + } + return serverIconPixels; + } + + public List 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerRateLimiter.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerRateLimiter.java new file mode 100644 index 0000000..1b3c9b5 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerRateLimiter.java @@ -0,0 +1,195 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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.md_5.bungee.config.Configuration; + +/** + * 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 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 exceptions; + + private EaglerRateLimiter(int period, int limit, int limitLockout, int lockoutDuration, List 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 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 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 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 exc = (Collection) config.getList("exceptions"); + List 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; + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerUpdateConfig.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerUpdateConfig.java new file mode 100644 index 0000000..cd58094 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/EaglerUpdateConfig.java @@ -0,0 +1,84 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config; + +import java.util.Collection; + +import net.md_5.bungee.config.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 downloadCertURLs = (Collection)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 downloadCertURLs; + + public EaglerUpdateConfig(boolean blockAllClientUpdates, boolean discardLoginPacketCerts, + int certPacketDataRateLimit, boolean enableEagcertFolder, boolean downloadLatestCerts, + int checkForUpdatesEvery, Collection 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 getDownloadCertURLs() { + return downloadCertURLs; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/MOTDCacheConfiguration.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/MOTDCacheConfiguration.java new file mode 100644 index 0000000..0e4bef1 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/MOTDCacheConfiguration.java @@ -0,0 +1,35 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/RateLimitStatus.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/RateLimitStatus.java new file mode 100644 index 0000000..20747cd --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/RateLimitStatus.java @@ -0,0 +1,20 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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; +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/ServerIconLoader.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/ServerIconLoader.java new file mode 100644 index 0000000..b0a0f43 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/ServerIconLoader.java @@ -0,0 +1,81 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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; + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/VanillaDefaultSkinProfileLoader.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/VanillaDefaultSkinProfileLoader.java new file mode 100644 index 0000000..f247ada --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/VanillaDefaultSkinProfileLoader.java @@ -0,0 +1,157 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import java.util.logging.Level; + +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_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.BinaryHttpClient; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.BinaryHttpClient.Response; +import net.md_5.bungee.protocol.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. + * + */ +class VanillaDefaultSkinProfileLoader implements Consumer { + + private class ProfileSkinConsumerImpl implements Consumer { + + private final String uuid; + + private ProfileSkinConsumerImpl(String uuid) { + this.uuid = uuid; + } + + @Override + public void accept(Response response) { + if(response == null) { + EaglerXBungee.logger().severe("Request error (null)"); + doNotify(); + }else if(response.exception != null) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception loading vanilla default profile!", response.exception); + doNotify(); + }else if(response.code != 200) { + EaglerXBungee.logger().severe("Recieved code " + response.code + " while looking up profile of " + uuid); + doNotify(); + }else if (response.data == null) { + EaglerXBungee.logger().severe("Recieved null payload while looking up profile of " + uuid); + doNotify(); + }else { + try { + JsonObject json = (new JsonParser()).parse(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() : null); + config.eaglerPlayersVanillaSkinCached = new Property[] { newProp, EaglerBungeeConfig.isEaglerProperty }; + } + EaglerXBungee.logger().info("Loaded vanilla profile: " + config.getEaglerPlayersVanillaSkin()); + doNotify(); + return; + } + } + } + } + } + EaglerXBungee.logger().warning("No skin was found for: " + config.getEaglerPlayersVanillaSkin()); + }catch(Throwable t) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception processing name to UUID lookup response!", t); + } + doNotify(); + } + } + + } + + private final EaglerBungeeConfig config; + private volatile boolean isLocked = true; + private final Object lock = new Object(); + + private VanillaDefaultSkinProfileLoader(EaglerBungeeConfig config) { + this.config = config; + } + + @Override + public void accept(Response response) { + if(response == null) { + EaglerXBungee.logger().severe("Request error (null)"); + doNotify(); + }else if(response.exception != null) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception loading vanilla default profile!", response.exception); + doNotify(); + }else if(response.code != 200) { + EaglerXBungee.logger().severe("Recieved code " + response.code + " while looking up UUID of " + config.getEaglerPlayersVanillaSkin()); + doNotify(); + }else if (response.data == null) { + EaglerXBungee.logger().severe("Recieved null payload while looking up UUID of " + config.getEaglerPlayersVanillaSkin()); + doNotify(); + }else { + try { + JsonObject json = (new JsonParser()).parse(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) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception processing name to UUID lookup response!", t); + doNotify(); + } + } + } + + private void doNotify() { + synchronized(lock) { + if(isLocked) { + isLocked = false; + lock.notify(); + } + } + } + + static void lookupVanillaSkinUser(EaglerBungeeConfig config) { + String user = config.getEaglerPlayersVanillaSkin(); + EaglerXBungee.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) { + EaglerXBungee.logger().warning("Profile load timed out"); + } + } + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/handlers/EaglerPacketEventListener.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/handlers/EaglerPacketEventListener.java new file mode 100644 index 0000000..fcd2740 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/handlers/EaglerPacketEventListener.java @@ -0,0 +1,205 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.handlers; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.logging.Level; + +import org.apache.commons.codec.binary.Base64; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.auth.DefaultAuthSystem; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerAuthConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.CapePackets; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.CapeServiceOffline; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.SkinPackets; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.SkinService; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.voice.VoiceService; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.voice.VoiceSignalPackets; +import net.md_5.bungee.UserConnection; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.connection.Server; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PluginMessageEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.event.ServerConnectedEvent; +import net.md_5.bungee.api.event.ServerDisconnectEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.connection.InitialHandler; +import net.md_5.bungee.connection.LoginResult; +import net.md_5.bungee.event.EventHandler; +import net.md_5.bungee.protocol.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 EaglerPacketEventListener implements Listener { + + public static final String FNAW_SKIN_ENABLE_CHANNEL = "EAG|FNAWSEn-1.8"; + + public final EaglerXBungee plugin; + + public EaglerPacketEventListener(EaglerXBungee plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onPluginMessage(final PluginMessageEvent event) { + if(event.getSender() instanceof UserConnection) { + final UserConnection player = (UserConnection)event.getSender(); + if(player.getPendingConnection() instanceof EaglerInitialHandler) { + if(SkinService.CHANNEL.equals(event.getTag())) { + event.setCancelled(true); + ProxyServer.getInstance().getScheduler().runAsync(plugin, new Runnable() { + @Override + public void run() { + try { + SkinPackets.processPacket(event.getData(), player, plugin.getSkinService()); + } catch (IOException e) { + event.getSender().disconnect(new TextComponent("Skin packet error!")); + EaglerXBungee.logger().log(Level.SEVERE, "Eagler user \"" + player.getName() + "\" raised an exception handling skins!", e); + } + } + }); + }else if(CapeServiceOffline.CHANNEL.equals(event.getTag())) { + event.setCancelled(true); + try { + CapePackets.processPacket(event.getData(), player, plugin.getCapeService()); + } catch (IOException e) { + event.getSender().disconnect(new TextComponent("Cape packet error!")); + EaglerXBungee.logger().log(Level.SEVERE, "Eagler user \"" + player.getName() + "\" raised an exception handling capes!", e); + } + }else if(VoiceService.CHANNEL.equals(event.getTag())) { + event.setCancelled(true); + VoiceService svc = plugin.getVoiceService(); + if(svc != null && ((EaglerInitialHandler)player.getPendingConnection()).getEaglerListenerConfig().getEnableVoiceChat()) { + try { + VoiceSignalPackets.processPacket(event.getData(), player, svc); + } catch (IOException e) { + event.getSender().disconnect(new TextComponent("Voice signal packet error!")); + EaglerXBungee.logger().log(Level.SEVERE, "Eagler user \"" + player.getName() + "\" raised an exception handling voice signals!", e); + } + } + } + } + }else if(event.getSender() instanceof Server && event.getReceiver() instanceof UserConnection) { + UserConnection player = (UserConnection)event.getReceiver(); + if("EAG|GetDomain".equals(event.getTag()) && player.getPendingConnection() instanceof EaglerInitialHandler) { + event.setCancelled(true); + String domain = ((EaglerInitialHandler)player.getPendingConnection()).getOrigin(); + if(domain == null) { + ((Server)event.getSender()).sendData("EAG|Domain", new byte[] { 0 }); + }else { + ((Server)event.getSender()).sendData("EAG|Domain", domain.getBytes(StandardCharsets.UTF_8)); + } + } + } + } + + @EventHandler + @SuppressWarnings("deprecation") + public void onPostLogin(PostLoginEvent event) { + ProxiedPlayer p = event.getPlayer(); + if(p instanceof UserConnection) { + UserConnection player = (UserConnection)p; + InitialHandler handler = player.getPendingConnection(); + LoginResult res = handler.getLoginProfile(); + if(res != null) { + Property[] props = res.getProperties(); + if(props.length > 0) { + for(int i = 0; i < props.length; ++i) { + Property pp = props[i]; + if(pp.getName().equals("textures")) { + try { + String jsonStr = SkinPackets.bytesToAscii(Base64.decodeBase64(pp.getValue())); + JsonObject json = (new JsonParser()).parse(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); + } + } + } + } + + @EventHandler + public void onConnectionLost(PlayerDisconnectEvent event) { + UUID uuid = event.getPlayer().getUniqueId(); + plugin.getSkinService().unregisterPlayer(uuid); + plugin.getCapeService().unregisterPlayer(uuid); + if(event.getPlayer() instanceof UserConnection) { + UserConnection player = (UserConnection)event.getPlayer(); + if((player.getPendingConnection() instanceof EaglerInitialHandler) + && ((EaglerInitialHandler) player.getPendingConnection()).getEaglerListenerConfig() + .getEnableVoiceChat()) { + plugin.getVoiceService().handlePlayerLoggedOut(player); + } + } + } + + @EventHandler + public void onServerConnected(ServerConnectedEvent event) { + if(event.getPlayer() instanceof UserConnection) { + UserConnection player = (UserConnection)event.getPlayer(); + if(player.getPendingConnection() instanceof EaglerInitialHandler) { + EaglerInitialHandler handler = (EaglerInitialHandler) player.getPendingConnection(); + ServerInfo sv = event.getServer().getInfo(); + boolean fnawSkins = !plugin.getConfig().getDisableFNAWSkinsEverywhere() && !plugin.getConfig().getDisableFNAWSkinsOnServersSet().contains(sv.getName()); + if(fnawSkins != handler.currentFNAWSkinEnableStatus) { + handler.currentFNAWSkinEnableStatus = fnawSkins; + player.sendData(FNAW_SKIN_ENABLE_CHANNEL, new byte[] { fnawSkins ? (byte)1 : (byte)0 }); + } + if(handler.getEaglerListenerConfig().getEnableVoiceChat()) { + plugin.getVoiceService().handleServerConnected(player, sv); + } + } + } + } + + @EventHandler + public void onServerDisconnected(ServerDisconnectEvent event) { + if(event.getPlayer() instanceof UserConnection) { + UserConnection player = (UserConnection)event.getPlayer(); + if((player.getPendingConnection() instanceof EaglerInitialHandler) + && ((EaglerInitialHandler) player.getPendingConnection()).getEaglerListenerConfig() + .getEnableVoiceChat()) { + plugin.getVoiceService().handleServerDisconnected(player, event.getTarget()); + } + } + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/handlers/EaglerPluginEventListener.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/handlers/EaglerPluginEventListener.java new file mode 100644 index 0000000..e4f9c21 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/handlers/EaglerPluginEventListener.java @@ -0,0 +1,36 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.handlers; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.md_5.bungee.api.event.ProxyReloadEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.event.EventHandler; + +/** + * 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 EaglerPluginEventListener implements Listener { + + public final EaglerXBungee plugin; + + public EaglerPluginEventListener(EaglerXBungee plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onReload(ProxyReloadEvent evt) { + plugin.reload(); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerChannelWrapper.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerChannelWrapper.java new file mode 100644 index 0000000..4740ecb --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerChannelWrapper.java @@ -0,0 +1,64 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import io.netty.channel.ChannelHandlerContext; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.bungeeprotocol.EaglerBungeeProtocol; +import net.md_5.bungee.netty.ChannelWrapper; +import net.md_5.bungee.protocol.Protocol; + +/** + * 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 EaglerChannelWrapper extends ChannelWrapper { + + public EaglerChannelWrapper(ChannelHandlerContext ctx) { + super(ctx); + } + + public void setProtocol(EaglerBungeeProtocol protocol) { + getHandle().pipeline().get(EaglerMinecraftEncoder.class).setProtocol(protocol); + getHandle().pipeline().get(EaglerMinecraftDecoder.class).setProtocol(protocol); + } + + public void setVersion(int protocol) { + getHandle().pipeline().get(EaglerMinecraftEncoder.class).setProtocolVersion(protocol); + getHandle().pipeline().get(EaglerMinecraftDecoder.class).setProtocolVersion(protocol); + } + + private Protocol lastProtocol = null; + + public Protocol getEncodeProtocol() { + EaglerMinecraftEncoder enc; + if (this.getHandle() == null || (enc = this.getHandle().pipeline().get(EaglerMinecraftEncoder.class)) == null) return lastProtocol; + EaglerBungeeProtocol eaglerProtocol = enc.getProtocol(); + switch(eaglerProtocol) { + case GAME: + return (lastProtocol = Protocol.GAME); + case HANDSHAKE: + return (lastProtocol = Protocol.HANDSHAKE); + case LOGIN: + return (lastProtocol = Protocol.LOGIN); + case STATUS: + return (lastProtocol = Protocol.STATUS); + default: + return lastProtocol; + } + } + + public void close(Object o) { + super.close(o); + EaglerPipeline.closeChannel(getHandle()); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerConnectionInstance.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerConnectionInstance.java new file mode 100644 index 0000000..24edffb --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerConnectionInstance.java @@ -0,0 +1,43 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import io.netty.channel.Channel; +import net.md_5.bungee.UserConnection; + +/** + * 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 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 UserConnection userConnection = null; + + public EaglerConnectionInstance(Channel channel) { + this.channel = channel; + this.creationTime = this.lastServerPingPacket = this.lastClientPingPacket = + this.lastClientPongPacket = System.currentTimeMillis(); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerInitialHandler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerInitialHandler.java new file mode 100644 index 0000000..c7377d0 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerInitialHandler.java @@ -0,0 +1,269 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import gnu.trove.set.TIntSet; +import gnu.trove.set.hash.TIntHashSet; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerBungeeConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.SimpleRateLimiter; +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.connection.InitialHandler; +import net.md_5.bungee.connection.LoginResult; +import net.md_5.bungee.netty.ChannelWrapper; +import net.md_5.bungee.protocol.DefinedPacket; +import net.md_5.bungee.protocol.PacketWrapper; +import net.md_5.bungee.protocol.Property; +import net.md_5.bungee.protocol.packet.EncryptionResponse; +import net.md_5.bungee.protocol.packet.Handshake; +import net.md_5.bungee.protocol.packet.LegacyHandshake; +import net.md_5.bungee.protocol.packet.LegacyPing; +import net.md_5.bungee.protocol.packet.LoginPayloadResponse; +import net.md_5.bungee.protocol.packet.LoginRequest; +import net.md_5.bungee.protocol.packet.PingPacket; +import net.md_5.bungee.protocol.packet.PluginMessage; +import net.md_5.bungee.protocol.packet.StatusRequest; + +/** + * 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 EaglerInitialHandler extends InitialHandler { + + public static class ClientCertificateHolder { + public final byte[] data; + public final int hash; + public ClientCertificateHolder(byte[] data, int hash) { + this.data = data; + this.hash = hash; + } + } + + private final int gameProtocolVersion; + private final String username; + private final UUID playerUUID; + private LoginResult loginResult; + private final InetSocketAddress eaglerAddress; + private final InetSocketAddress virtualHost; + private final Unsafe eaglerUnsafe; + public final SimpleRateLimiter skinLookupRateLimiter; + public final SimpleRateLimiter skinUUIDLookupRateLimiter; + public final SimpleRateLimiter skinTextureDownloadRateLimiter; + public final SimpleRateLimiter capeLookupRateLimiter; + public final SimpleRateLimiter voiceConnectRateLimiter; + public final String origin; + public final ClientCertificateHolder clientCertificate; + public final Set certificatesToSend; + public final TIntSet certificatesSent; + public boolean currentFNAWSkinEnableStatus = true; + + private static final Property[] NO_PROPERTIES = new Property[0]; + + public EaglerInitialHandler(BungeeCord bungee, EaglerListenerConfig listener, final ChannelWrapper ch, + int gameProtocolVersion, String username, UUID playerUUID, InetSocketAddress address, String host, + String origin, ClientCertificateHolder clientCertificate) { + super(bungee, listener); + this.gameProtocolVersion = gameProtocolVersion; + this.username = username; + this.playerUUID = playerUUID; + this.eaglerAddress = address; + 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 TIntHashSet(); + if(clientCertificate != null) { + this.certificatesSent.add(clientCertificate.hashCode()); + } + if(host == null) host = ""; + int port = 25565; + if(host.contains(":")) { + int ind = host.lastIndexOf(':'); + try { + port = Integer.parseInt(host.substring(ind + 1)); + host = host.substring(0, ind); + } catch (NumberFormatException e) { + // + } + } + this.virtualHost = InetSocketAddress.createUnresolved(host, port); + this.eaglerUnsafe = new Unsafe() { + @Override + public void sendPacket(DefinedPacket arg0) { + ch.getHandle().writeAndFlush(arg0); + } + }; + Property[] profileProperties = NO_PROPERTIES; + if(EaglerXBungee.getEagler().getConfig().getEnableIsEaglerPlayerProperty()) { + profileProperties = new Property[] { EaglerBungeeConfig.isEaglerProperty }; + } + setLoginProfile(new LoginResult(playerUUID.toString(), username, profileProperties)); + try { + super.connected(ch); + } catch (Exception e) { + } + } + + void setLoginProfile(LoginResult obj) { + this.loginResult = obj; + try { + Field f = InitialHandler.class.getDeclaredField("loginProfile"); + f.setAccessible(true); + f.set(this, obj); + }catch(Throwable t) { + } + } + + @Override + public void handle(PacketWrapper packet) throws Exception { + } + + @Override + public void handle(PluginMessage pluginMessage) throws Exception { + } + + @Override + public void handle(LegacyHandshake legacyHandshake) throws Exception { + } + + @Override + public void handle(LegacyPing ping) throws Exception { + } + + @Override + public void handle(StatusRequest statusRequest) throws Exception { + } + + @Override + public void handle(PingPacket ping) throws Exception { + } + + @Override + public void handle(Handshake handshake) throws Exception { + } + + @Override + public void handle(LoginRequest loginRequest) throws Exception { + } + + @Override + public void handle(EncryptionResponse encryptResponse) throws Exception { + } + + @Override + public void handle(LoginPayloadResponse response) throws Exception { + } + + @Override + public void disconnect(String reason) { + super.disconnect(reason); + } + + @Override + public void disconnect(BaseComponent... reason) { + super.disconnect(reason); + } + + @Override + public void disconnect(BaseComponent reason) { + super.disconnect(reason); + } + + @Override + public String getName() { + return username; + } + + @Override + public int getVersion() { + return gameProtocolVersion; + } + + @Override + public Handshake getHandshake() { + return new Handshake(gameProtocolVersion, virtualHost.getHostName(), virtualHost.getPort(), + gameProtocolVersion); + } + + @Override + public LoginRequest getLoginRequest() { + throw new UnsupportedOperationException("A plugin attempted to retrieve the LoginRequest packet from an EaglercraftX connection, " + + "which is not supported because Eaglercraft does not use online mode encryption. Please analyze the stack trace of this " + + "exception and reconfigure or remove the offending plugin to fix this issue."); + } + + @Override + public PluginMessage getBrandMessage() { + String brand = "EaglercraftX"; + byte[] pkt = new byte[brand.length() + 1]; + pkt[0] = (byte)brand.length(); + System.arraycopy(brand.getBytes(StandardCharsets.US_ASCII), 0, pkt, 1, brand.length()); + return new PluginMessage("MC|Brand", pkt, true); + } + + @Override + public UUID getUniqueId() { + return playerUUID; + } + + @Override + public UUID getOfflineId() { + return playerUUID; + } + + @Override + public String getUUID() { + return playerUUID.toString().replace("-", ""); + } + + @Override + public LoginResult getLoginProfile() { + return loginResult; + } + + @Override + public InetSocketAddress getVirtualHost() { + return virtualHost; + } + + @Override + public SocketAddress getSocketAddress() { + return eaglerAddress; + } + + @Override + public Unsafe unsafe() { + return eaglerUnsafe; + } + + public String getOrigin() { + return origin; + } + + public EaglerListenerConfig getEaglerListenerConfig() { + return (EaglerListenerConfig)getListener(); + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftByteBufEncoder.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftByteBufEncoder.java new file mode 100644 index 0000000..1c0d2bb --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftByteBufEncoder.java @@ -0,0 +1,33 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import java.util.List; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; + +/** + * 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 EaglerMinecraftByteBufEncoder extends MessageToMessageEncoder { + + @Override + protected void encode(ChannelHandlerContext var1, ByteBuf var2, List var3) throws Exception { + var3.add(new BinaryWebSocketFrame(Unpooled.copiedBuffer(var2))); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftDecoder.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftDecoder.java new file mode 100644 index 0000000..f5ddadc --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftDecoder.java @@ -0,0 +1,148 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import io.netty.buffer.ByteBuf; +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 net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.bungeeprotocol.EaglerBungeeProtocol; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.bungeeprotocol.EaglerProtocolAccessProxy; +import net.md_5.bungee.protocol.DefinedPacket; +import net.md_5.bungee.protocol.PacketWrapper; +import net.md_5.bungee.protocol.Protocol; +import net.md_5.bungee.protocol.ProtocolConstants.Direction; + +/** + * 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 EaglerMinecraftDecoder extends MessageToMessageDecoder { + private EaglerBungeeProtocol protocol; + private final boolean server; + private int protocolVersion; + private static Constructor packetWrapperConstructor = null; + + @Override + protected void decode(ChannelHandlerContext ctx, WebSocketFrame frame, List out) throws Exception { + if(!ctx.channel().isActive()) { + return; + } + EaglerConnectionInstance con = ctx.channel().attr(EaglerPipeline.CONNECTION_INSTANCE).get(); + long millis = System.currentTimeMillis(); + if(frame instanceof BinaryWebSocketFrame) { + BinaryWebSocketFrame in = (BinaryWebSocketFrame) frame; + ByteBuf buf = in.content(); + int pktId = DefinedPacket.readVarInt(buf); + DefinedPacket pkt = EaglerProtocolAccessProxy.createPacket(protocol, protocolVersion, pktId, server); + Protocol bungeeProtocol = null; + switch(this.protocol) { + case GAME: + bungeeProtocol = Protocol.GAME; + break; + case HANDSHAKE: + bungeeProtocol = Protocol.HANDSHAKE; + break; + case LOGIN: + bungeeProtocol = Protocol.LOGIN; + break; + case STATUS: + bungeeProtocol = Protocol.STATUS; + } + if(pkt != null) { + pkt.read(buf, server ? Direction.TO_CLIENT : Direction.TO_SERVER, protocolVersion); + if(buf.isReadable()) { + EaglerXBungee.logger().severe("[DECODER][" + ctx.channel().remoteAddress() + "] Packet " + + pkt.getClass().getSimpleName() + " had extra bytes! (" + buf.readableBytes() + ")"); + }else { + out.add(this.wrapPacket(pkt, buf, bungeeProtocol)); + } + }else { + out.add(this.wrapPacket(null, buf, bungeeProtocol)); + } + }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(); + } + } + + public EaglerMinecraftDecoder(final EaglerBungeeProtocol protocol, final boolean server, final int protocolVersion) { + this.protocol = protocol; + this.server = server; + this.protocolVersion = protocolVersion; + } + + public void setProtocol(final EaglerBungeeProtocol protocol) { + this.protocol = protocol; + } + + public void setProtocolVersion(final int protocolVersion) { + this.protocolVersion = protocolVersion; + } + + + + private PacketWrapper wrapPacket(DefinedPacket packet, ByteBuf buf, Protocol protocol) { + ByteBuf cbuf = null; + + PacketWrapper var7; + try { + cbuf = buf.copy(0, buf.writerIndex()); + PacketWrapper pkt; + if (packetWrapperConstructor != null) { + try { + pkt = packetWrapperConstructor.newInstance(packet, cbuf); + cbuf = null; + return pkt; + } catch (IllegalAccessException | InvocationTargetException | InstantiationException var14) { + throw new RuntimeException(var14); + } + } + + try { + pkt = new PacketWrapper(packet, cbuf, protocol); + cbuf = null; + return pkt; + } catch (NoSuchMethodError var15) { + try { + packetWrapperConstructor = PacketWrapper.class.getDeclaredConstructor(DefinedPacket.class, ByteBuf.class); + pkt = packetWrapperConstructor.newInstance(packet, cbuf); + cbuf = null; + var7 = pkt; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | InstantiationException var13) { + throw new RuntimeException(var13); + } + } + } finally { + if (cbuf != null) { + cbuf.release(); + } + + } + + return var7; + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftEncoder.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftEncoder.java new file mode 100644 index 0000000..323a77b --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftEncoder.java @@ -0,0 +1,95 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import java.lang.reflect.Method; +import java.util.List; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.bungeeprotocol.EaglerBungeeProtocol; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.bungeeprotocol.EaglerProtocolAccessProxy; +import net.md_5.bungee.protocol.DefinedPacket; +import net.md_5.bungee.protocol.Protocol; +import net.md_5.bungee.protocol.ProtocolConstants.Direction; + +/** + * 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 EaglerMinecraftEncoder extends MessageToMessageEncoder { + + private EaglerBungeeProtocol protocol; + private boolean server; + private int protocolVersion; + private static Method meth = null; + + @Override + protected void encode(ChannelHandlerContext ctx, DefinedPacket msg, List out) throws Exception { + Protocol bungeeProtocol = null; + switch(this.protocol) { + case GAME: + bungeeProtocol = Protocol.GAME; + break; + case HANDSHAKE: + bungeeProtocol = Protocol.HANDSHAKE; + break; + case LOGIN: + bungeeProtocol = Protocol.LOGIN; + break; + case STATUS: + bungeeProtocol = Protocol.STATUS; + } + ByteBuf buf = Unpooled.buffer(); + int pk = EaglerProtocolAccessProxy.getPacketId(protocol, protocolVersion, msg, server); + DefinedPacket.writeVarInt(pk, buf); + try { + msg.write(buf, bungeeProtocol, server ? Direction.TO_CLIENT : Direction.TO_SERVER, protocolVersion); + } catch (NoSuchMethodError e) { + try { + if (meth == null) { + meth = DefinedPacket.class.getDeclaredMethod("write", ByteBuf.class, Direction.class, int.class); + } + meth.invoke(msg, buf, server ? Direction.TO_CLIENT : Direction.TO_SERVER, protocolVersion); + } catch (Exception e1) { + buf.release(); + buf = Unpooled.EMPTY_BUFFER; + } + } catch (Exception e) { + buf.release(); + buf = Unpooled.EMPTY_BUFFER; + } + out.add(new BinaryWebSocketFrame(buf)); + } + + public EaglerMinecraftEncoder(final EaglerBungeeProtocol protocol, final boolean server, final int protocolVersion) { + this.protocol = protocol; + this.server = server; + this.protocolVersion = protocolVersion; + } + + public void setProtocol(final EaglerBungeeProtocol protocol) { + this.protocol = protocol; + } + + public void setProtocolVersion(final int protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public EaglerBungeeProtocol getProtocol() { + return this.protocol; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftWrappedEncoder.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftWrappedEncoder.java new file mode 100644 index 0000000..d7689a3 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerMinecraftWrappedEncoder.java @@ -0,0 +1,32 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import java.util.List; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import net.md_5.bungee.protocol.PacketWrapper; + +/** + * 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 EaglerMinecraftWrappedEncoder extends MessageToMessageEncoder { + + @Override + protected void encode(ChannelHandlerContext ctx, PacketWrapper msg, List out) throws Exception { + out.add(new BinaryWebSocketFrame(msg.buf)); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerPipeline.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerPipeline.java new file mode 100644 index 0000000..f4843b9 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerPipeline.java @@ -0,0 +1,185 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 java.util.logging.Logger; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +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_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerBungeeConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler.ClientCertificateHolder; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 EaglerPipeline { + + public static final AttributeKey LISTENER = AttributeKey.valueOf("ListenerInfo"); + public static final AttributeKey LOCAL_ADDRESS = AttributeKey.valueOf("LocalAddress"); + public static final AttributeKey CONNECTION_INSTANCE = AttributeKey.valueOf("EaglerConnectionInstance"); + public static final AttributeKey REAL_ADDRESS = AttributeKey.valueOf("RealAddress"); + public static final AttributeKey HOST = AttributeKey.valueOf("Host"); + public static final AttributeKey ORIGIN = AttributeKey.valueOf("Origin"); + + public static final Collection openChannels = new LinkedList(); + + public static final String UPDATE_CERT_CHANNEL = "EAG|UpdateCert-1.8"; + + public static final TimerTask closeInactive = new TimerTask() { + + @Override + public void run() { + Logger log = EaglerXBungee.logger(); + try { + EaglerBungeeConfig conf = EaglerXBungee.getEagler().getConfig(); + long handshakeTimeout = conf.getWebsocketHandshakeTimeout(); + long keepAliveTimeout = conf.getWebsocketKeepAliveTimeout(); + List channelsList; + synchronized(openChannels) { + long millis = System.currentTimeMillis(); + Iterator 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.severe("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; + } + EaglerInitialHandler i = (EaglerInitialHandler)conn.userConnection.getPendingConnection(); + ClientCertificateHolder certHolder = null; + synchronized(i.certificatesToSend) { + if(i.certificatesToSend.size() > 0) { + Iterator 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.sendData(UPDATE_CERT_CHANNEL, certHolder.data); + sizeTracker += certHolder.data.length; + if(sizeTracker > (conf.getUpdateConfig().getCertPacketDataRateLimit() / 4)) { + break; + } + } + } + } + EaglerUpdateSvc.updateTick(); + } + }catch(Throwable t) { + log.severe("Exception in thread \"" + Thread.currentThread().getName() + "\"! " + t.toString()); + t.printStackTrace(); + } + } + }; + + public static final ChannelInitializer SERVER_CHILD = new ChannelInitializer() { + + @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 = EaglerXBungee.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); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerUpdateSvc.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerUpdateSvc.java new file mode 100644 index 0000000..a656c66 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/EaglerUpdateSvc.java @@ -0,0 +1,297 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 java.util.logging.Logger; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.auth.SHA1Digest; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerUpdateConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler.ClientCertificateHolder; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +/** + * 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 certs = new ArrayList(); + private static final Map 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 = EaglerXBungee.logger(); + long millis = System.currentTimeMillis(); + EaglerUpdateConfig conf = EaglerXBungee.getEagler().getConfig().getUpdateConfig(); + if(conf.isDownloadLatestCerts() && millis - lastDownload > (long)conf.getCheckForUpdatesEvery() * 1000l) { + lastDownload = millis; + lastEnumerate = 0l; + try { + downloadUpdates(); + }catch(Throwable t) { + log.severe("Uncaught exception downloading certificates!"); + t.printStackTrace(); + } + millis = System.currentTimeMillis(); + } + if(conf.isEnableEagcertFolder() && millis - lastEnumerate > 5000l) { + lastEnumerate = millis; + try { + enumerateEagcertDirectory(); + }catch(Throwable t) { + log.severe("Uncaught exception reading eagcert directory!"); + t.printStackTrace(); + } + } + } + + private static void downloadUpdates() throws Throwable { + Logger log = EaglerXBungee.logger(); + EaglerUpdateConfig conf = EaglerXBungee.getEagler().getConfig().getUpdateConfig(); + File eagcert = new File(EaglerXBungee.getEagler().getDataFolder(), "eagcert"); + if(!eagcert.isDirectory()) { + if(!eagcert.mkdirs()) { + log.severe("Could not create directory: " + eagcert.getAbsolutePath()); + return; + } + } + Set 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 EaglerXBungee/" + EaglerXBungee.getEagler().getDescription().getVersion()); + 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.severe("Failed to download certificate: " + str); + log.severe("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.warning("Deleting stale certificate: " + n); + if(!f.delete()) { + log.severe("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 = EaglerXBungee.logger(); + File eagcert = new File(EaglerXBungee.getEagler().getDataFolder(), "eagcert"); + if(!eagcert.isDirectory()) { + if(!eagcert.mkdirs()) { + log.severe("Could not create directory: " + eagcert.getAbsolutePath()); + return; + } + } + boolean dirty = false; + File[] dirList = eagcert.listFiles(); + Set 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.severe("Failed to read: " + f.getAbsolutePath()); + log.severe("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.severe("Failed to read: " + f.getAbsolutePath()); + log.severe("Reason: " + ex.toString()); + } + } + Iterator itr = certsCache.keySet().iterator(); + while(itr.hasNext()) { + String etr = itr.next(); + if(!existingFiles.contains(etr)) { + itr.remove(); + dirty = true; + log.warning("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 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(ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + if(p.getPendingConnection() instanceof EaglerInitialHandler) { + EaglerInitialHandler pp = (EaglerInitialHandler)p.getPendingConnection(); + 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(ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + if(p.getPendingConnection() instanceof EaglerInitialHandler) { + EaglerInitialHandler pp = (EaglerInitialHandler)p.getPendingConnection(); + boolean bb; + synchronized(pp.certificatesSent) { + bb = pp.certificatesSent.contains(holder.hashCode()); + } + if(!bb) { + Set set = pp.certificatesToSend; + synchronized(set) { + set.add(holder); + } + } + } + } + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HandshakePacketTypes.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HandshakePacketTypes.java new file mode 100644 index 0000000..107d359 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HandshakePacketTypes.java @@ -0,0 +1,49 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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; + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpHandshakeHandler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpHandshakeHandler.java new file mode 100644 index 0000000..f81434c --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpHandshakeHandler.java @@ -0,0 +1,185 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; + +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 net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerRateLimiter; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.RateLimitStatus; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.web.HttpMemoryCache; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.web.HttpWebServer; + +/** + * 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 HttpHandshakeHandler extends ChannelInboundHandlerAdapter { + + private static final byte[] error429Bytes = "

429 Too Many Requests
(Try again later)

".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 { + 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) { + EaglerXBungee.logger().warning("[" + ctx.channel().remoteAddress() + "]: Connected with an invalid '" + conf.getForwardIpHeader() + "' header, disconnecting..."); + ctx.close(); + return; + } + }else { + EaglerXBungee.logger().warning("[" + 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(); + } + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (ctx.channel().isActive()) { + EaglerXBungee.logger().log(Level.WARNING, "[Pre][" + ctx.channel().remoteAddress() + "]: Exception Caught: " + cause.toString(), cause); + ctx.close(); + } + } + + private static String formatAddressFor404(String str) { + return "" + str.replace("<", "<").replace(">", ">") + ""; + } + + public void channelInactive(ChannelHandlerContext ctx) { + EaglerPipeline.closeChannel(ctx.channel()); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpServerQueryHandler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpServerQueryHandler.java new file mode 100644 index 0000000..69fb3b8 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpServerQueryHandler.java @@ -0,0 +1,232 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 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 { + 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 { + EaglerXBungee.logger().severe("Unexpected Packet: " + msg.getClass().getSimpleName()); + } + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (ctx.channel().isActive()) { + EaglerXBungee.logger().warning("[" + ctx.channel().remoteAddress() + "]: Exception Caught: " + 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 = (new JsonParser()).parse(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) { + ByteBuf buf = Unpooled.buffer(bytes.length, bytes.length); + buf.writeBytes(bytes); + context.writeAndFlush(new BinaryWebSocketFrame(buf)); + } + + public void sendBinaryResponseAndClose(byte[] bytes) { + ByteBuf buf = Unpooled.buffer(bytes.length, bytes.length); + buf.writeBytes(bytes); + context.writeAndFlush(new BinaryWebSocketFrame(buf)).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(); + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpWebSocketHandler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpWebSocketHandler.java new file mode 100644 index 0000000..aa27e95 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/HttpWebSocketHandler.java @@ -0,0 +1,1243 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import org.apache.commons.codec.binary.Base64; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; +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.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftHandleAuthPasswordEvent; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftIsAuthRequiredEvent; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftIsAuthRequiredEvent.AuthMethod; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftIsAuthRequiredEvent.AuthResponse; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftMOTDEvent; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.event.EaglercraftRegisterSkinEvent; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.auth.DefaultAuthSystem; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.command.CommandConfirmCode; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerAuthConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerBungeeConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerRateLimiter; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerUpdateConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.RateLimitStatus; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler.ClientCertificateHolder; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.bungeeprotocol.EaglerBungeeProtocol; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.query.MOTDQueryHandler; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.query.QueryManager; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.CapePackets; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.SkinPackets; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.SkinService; +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.UserConnection; +import net.md_5.bungee.api.AbstractReconnectHandler; +import net.md_5.bungee.api.Callback; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.LoginEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.event.PreLoginEvent; +import net.md_5.bungee.chat.ComponentSerializer; +import net.md_5.bungee.connection.LoginResult; +import net.md_5.bungee.connection.UpstreamBridge; +import net.md_5.bungee.netty.ChannelWrapper; +import net.md_5.bungee.netty.HandlerBoss; +import net.md_5.bungee.protocol.Property; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.event.ServerConnectEvent; + +/** + * 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 HttpWebSocketHandler extends ChannelInboundHandlerAdapter { + + private final EaglerListenerConfig conf; + + private int clientLoginState = HandshakePacketTypes.STATE_OPENED; + private int clientProtocolVersion = -1; + private boolean isProtocolExchanged = false; + private int gameProtocolVersion = -1; + private CharSequence clientBrandString; + private CharSequence clientVersionString; + private CharSequence clientUsername; + private UUID clientUUID; + private CharSequence clientRequestedServer; + private boolean clientAuth; + private byte[] clientAuthUsername; + private byte[] clientAuthPassword; + private EaglercraftIsAuthRequiredEvent authRequireEvent; + private final Map profileData = new HashMap(); + private boolean hasFirstPacket = false; + private boolean hasBinaryConnection = false; + private boolean connectionClosed = false; + private InetAddress remoteAddress; + private String localAddrString; + private Property texturesOverrideProperty; + private boolean overrideEaglerToVanillaSkins; + + public HttpWebSocketHandler(EaglerListenerConfig conf) { + this.conf = conf; + } + + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + 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 { + EaglerXBungee.logger().severe("Unexpected Packet: " + msg.getClass().getSimpleName()); + } + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (ctx.channel().isActive()) { + EaglerXBungee.logger().warning("[Yee][" + ctx.channel().remoteAddress() + "]: Exception Caught: " + cause.toString()); + } + } + + private void handleBinary(ChannelHandlerContext ctx, ByteBuf buffer) { + if(connectionClosed) { + return; + } + if(!hasFirstPacket) { + if(buffer.readableBytes() >= 2) { + if(buffer.getByte(0) == (byte)2 && buffer.getByte(1) == (byte)69) { + handleLegacyClient(ctx, buffer); + return; + } + } + hasFirstPacket = true; + hasBinaryConnection = true; + + BungeeCord bungus = BungeeCord.getInstance(); + int limit = bungus.config.getPlayerLimit(); + if (limit > 0 && bungus.getOnlineCount() >= limit) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, bungus.getTranslation("proxy_full")) + .addListener(ChannelFutureListener.CLOSE); + connectionClosed = true; + return; + } + + if(conf.getMaxPlayer() > 0) { + int i = 0; + for(ProxiedPlayer p : bungus.getPlayers()) { + if(p.getPendingConnection().getListener() == conf) { + ++i; + } + } + + if (i >= conf.getMaxPlayer()) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, bungus.getTranslation("proxy_full")) + .addListener(ChannelFutureListener.CLOSE); + connectionClosed = true; + return; + } + } + + SocketAddress localSocketAddr = ctx.channel().remoteAddress(); + InetAddress addr = ctx.channel().attr(EaglerPipeline.REAL_ADDRESS).get(); + + String limiterAddress = null; + RateLimitStatus loginRateLimit = RateLimitStatus.OK; + if(addr != null) { + remoteAddress = addr; + limiterAddress = addr.getHostAddress(); + }else { + if(localSocketAddr instanceof InetSocketAddress) { + remoteAddress = ((InetSocketAddress)localSocketAddr).getAddress(); + limiterAddress = remoteAddress.getHostAddress(); + }else { + remoteAddress = InetAddress.getLoopbackAddress(); + } + } + + EaglerRateLimiter limiter = conf.getRatelimitLogin(); + if(limiterAddress != null && limiter != null) { + loginRateLimit = limiter.rateLimit(limiterAddress); + } + + if(loginRateLimit == RateLimitStatus.LOCKED_OUT) { + ctx.close(); + connectionClosed = true; + return; + } + + if (loginRateLimit != RateLimitStatus.OK) { + sendErrorCode(ctx, + loginRateLimit == RateLimitStatus.LIMITED_NOW_LOCKED_OUT + ? HandshakePacketTypes.SERVER_ERROR_RATELIMIT_LOCKED + : HandshakePacketTypes.SERVER_ERROR_RATELIMIT_BLOCKED, + "Too many logins!").addListener(ChannelFutureListener.CLOSE); + connectionClosed = true; + return; + } + + localAddrString = localSocketAddr.toString(); + EaglerXBungee.logger().info("[" + localAddrString + "]: Connected via websocket"); + if(addr != null) { + EaglerXBungee.logger().info("[" + localAddrString + "]: Real address is " + addr.getHostAddress()); + } + String origin = ctx.channel().attr(EaglerPipeline.ORIGIN).get(); + if(origin != null) { + EaglerXBungee.logger().info("[" + localAddrString + "]: Origin header is " + origin); + }else { + EaglerXBungee.logger().info("[" + localAddrString + "]: No origin header is present!"); + } + }else if(!hasBinaryConnection) { + connectionClosed = true; + ctx.close(); + return; + } + int op = -1; + try { + op = buffer.readUnsignedByte(); + switch(op) { + case HandshakePacketTypes.PROTOCOL_CLIENT_VERSION: { + if(clientLoginState == HandshakePacketTypes.STATE_OPENED) { + clientLoginState = HandshakePacketTypes.STATE_STALLING; + EaglerXBungee eaglerXBungee = EaglerXBungee.getEagler(); + EaglerAuthConfig authConfig = eaglerXBungee.getConfig().getAuthConfig(); + + final int minecraftProtocolVersion = 47; + + int eaglerLegacyProtocolVersion = buffer.readUnsignedByte(); + + if(eaglerLegacyProtocolVersion == 1) { + if(authConfig.isEnableAuthentication()) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, "Please update your client to register on this server!") + .addListener(ChannelFutureListener.CLOSE); + return; + }else if(buffer.readUnsignedByte() != minecraftProtocolVersion) { + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + ByteBuf buf = Unpooled.buffer(); + buf.writeByte(HandshakePacketTypes.PROTOCOL_VERSION_MISMATCH); + buf.writeByte(1); + buf.writeByte(1); + buf.writeByte(eaglerLegacyProtocolVersion); + String str = "Outdated Client"; + buf.writeByte(str.length()); + buf.writeCharSequence(str, StandardCharsets.US_ASCII); + ctx.writeAndFlush(new BinaryWebSocketFrame(buf)).addListener(ChannelFutureListener.CLOSE); + return; + } + }else if(eaglerLegacyProtocolVersion == 2) { + int minProtVers = Integer.MAX_VALUE; + int maxProtVers = -1; + boolean hasV2InList = false; + boolean hasV3InList = false; + + int minGameVers = Integer.MAX_VALUE; + int maxGameVers = -1; + boolean has47InList = false; + + int cnt = buffer.readUnsignedShort(); + for(int i = 0; i < cnt; ++i) { + int j = buffer.readUnsignedShort(); + if(j == 2) { + hasV2InList = true; + } + if(j == 3) { + hasV3InList = true; + } + if(j > maxProtVers) { + maxProtVers = j; + } + if(j < minProtVers) { + minProtVers = j; + } + } + + cnt = buffer.readUnsignedShort(); + for(int i = 0; i < cnt; ++i) { + int j = buffer.readUnsignedShort(); + if(j == minecraftProtocolVersion) { + has47InList = true; + } + if(j > maxGameVers) { + maxGameVers = j; + } + if(j < minGameVers) { + minGameVers = j; + } + } + + if(minProtVers == Integer.MAX_VALUE || minGameVers == Integer.MAX_VALUE) { + throw new IOException(); + } + + boolean versMisMatch = false; + boolean isServerProbablyOutdated = false; + boolean isClientProbablyOutdated = false; + if(!hasV2InList && !hasV3InList) { + versMisMatch = true; + isServerProbablyOutdated = minProtVers > 3 && maxProtVers > 3; //make sure to update VersionQueryHandler too + isClientProbablyOutdated = minProtVers < 2 && maxProtVers < 2; + }else if(!has47InList) { + versMisMatch = true; + isServerProbablyOutdated = minGameVers > minecraftProtocolVersion && maxGameVers > minecraftProtocolVersion; + isClientProbablyOutdated = minGameVers < minecraftProtocolVersion && maxGameVers < minecraftProtocolVersion; + } + + clientProtocolVersion = hasV3InList ? 3 : 2; + + if(versMisMatch) { + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + ByteBuf buf = Unpooled.buffer(); + buf.writeByte(HandshakePacketTypes.PROTOCOL_VERSION_MISMATCH); + + buf.writeShort(2); + buf.writeShort(2); // want v2 or v3 + buf.writeShort(3); + + buf.writeShort(1); + buf.writeShort(minecraftProtocolVersion); // want game version 47 + + String str = isClientProbablyOutdated ? "Outdated Client" : (isServerProbablyOutdated ? "Outdated Server" : "Unsupported Client Version"); + buf.writeByte(str.length()); + buf.writeCharSequence(str, StandardCharsets.US_ASCII); + ctx.writeAndFlush(new BinaryWebSocketFrame(buf)).addListener(ChannelFutureListener.CLOSE); + return; + } + }else { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, "Legacy protocol version should always be '2' on post-snapshot clients") + .addListener(ChannelFutureListener.CLOSE); + return; + } + + int strlen = buffer.readUnsignedByte(); + CharSequence eaglerBrand = buffer.readCharSequence(strlen, StandardCharsets.US_ASCII); + strlen = buffer.readUnsignedByte(); + CharSequence eaglerVersionString = buffer.readCharSequence(strlen, StandardCharsets.US_ASCII); + + if(eaglerLegacyProtocolVersion >= 2) { + clientAuth = buffer.readBoolean(); + strlen = buffer.readUnsignedByte(); + clientAuthUsername = new byte[strlen]; + buffer.readBytes(clientAuthUsername); + } + if(buffer.isReadable()) { + throw new IllegalArgumentException("Packet too long"); + } + + boolean useSnapshotFallbackProtocol = false; + if(eaglerLegacyProtocolVersion == 1 && !authConfig.isEnableAuthentication()) { + clientProtocolVersion = 2; + useSnapshotFallbackProtocol = true; + clientAuth = false; + clientAuthUsername = null; + } + + InetAddress addr = ctx.channel().attr(EaglerPipeline.REAL_ADDRESS).get(); + if(addr == null) { + SocketAddress remoteSocketAddr = ctx.channel().remoteAddress(); + if(remoteSocketAddr instanceof InetSocketAddress) { + addr = ((InetSocketAddress)remoteSocketAddr).getAddress(); + }else { + addr = InetAddress.getLoopbackAddress(); + } + } + + final boolean final_useSnapshotFallbackProtocol = useSnapshotFallbackProtocol; + Runnable continueThread = () -> { + clientLoginState = HandshakePacketTypes.STATE_CLIENT_VERSION; + gameProtocolVersion = 47; + clientBrandString = eaglerBrand; + clientVersionString = eaglerVersionString; + + ByteBuf buf = Unpooled.buffer(); + buf.writeByte(HandshakePacketTypes.PROTOCOL_SERVER_VERSION); + + if(final_useSnapshotFallbackProtocol) { + buf.writeByte(1); + }else { + buf.writeShort(clientProtocolVersion); + buf.writeShort(minecraftProtocolVersion); + } + + String brandStr = eaglerXBungee.getDescription().getName(); + buf.writeByte(brandStr.length()); + buf.writeCharSequence(brandStr, StandardCharsets.US_ASCII); + + String versStr = eaglerXBungee.getDescription().getVersion(); + buf.writeByte(versStr.length()); + buf.writeCharSequence(versStr, StandardCharsets.US_ASCII); + + if(!authConfig.isEnableAuthentication() || !clientAuth) { + buf.writeByte(0); + buf.writeShort(0); + }else { + int meth = getAuthMethodId(authRequireEvent.getUseAuthType()); + + if(meth == -1) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, "Unsupported authentication method resolved") + .addListener(ChannelFutureListener.CLOSE); + EaglerXBungee.logger().severe("[" + localAddrString + "]: Disconnecting, unsupported AuthMethod: " + authRequireEvent.getUseAuthType()); + return; + } + + buf.writeByte(meth); + + byte[] saltingData = authRequireEvent.getSaltingData(); + if(saltingData != null) { + buf.writeShort(saltingData.length); + buf.writeBytes(saltingData); + }else { + buf.writeShort(0); + } + } + + ctx.writeAndFlush(new BinaryWebSocketFrame(buf)); + isProtocolExchanged = true; + }; + + authRequireEvent = null; + if(authConfig.isEnableAuthentication()) { + String origin = ctx.channel().attr(EaglerPipeline.ORIGIN).get(); + try { + authRequireEvent = new EaglercraftIsAuthRequiredEvent(conf, remoteAddress, origin, + clientAuth, clientAuthUsername, (reqAuthEvent) -> { + if(authRequireEvent.shouldKickUser()) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, authRequireEvent.getKickMessage()) + .addListener(ChannelFutureListener.CLOSE); + return; + } + + AuthResponse resp = authRequireEvent.getAuthRequired(); + if(resp == null) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, "IsAuthRequiredEvent was not handled") + .addListener(ChannelFutureListener.CLOSE); + EaglerXBungee.logger().severe("[" + localAddrString + "]: Disconnecting, no installed authentication system handled: " + authRequireEvent.toString()); + return; + } + + if(resp == AuthResponse.DENY) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, authRequireEvent.getKickMessage()) + .addListener(ChannelFutureListener.CLOSE); + return; + } + + AuthMethod type = authRequireEvent.getUseAuthType(); + if(type == null) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, "IsAuthRequiredEvent was not fully handled") + .addListener(ChannelFutureListener.CLOSE); + EaglerXBungee.logger().severe("[" + localAddrString + "]: Disconnecting, no authentication method provided by handler"); + return; + } + + int typeId = getAuthMethodId(type); + if(typeId == -1) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, "Unsupported authentication method resolved") + .addListener(ChannelFutureListener.CLOSE); + EaglerXBungee.logger().severe("[" + localAddrString + "]: Disconnecting, unsupported AuthMethod: " + type); + return; + } + + if(!clientAuth && resp == AuthResponse.REQUIRE) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_AUTHENTICATION_REQUIRED, + HandshakePacketTypes.AUTHENTICATION_REQUIRED + " [" + typeId + "] " + authRequireEvent.getAuthMessage()) + .addListener(ChannelFutureListener.CLOSE); + EaglerXBungee.logger().info("[" + localAddrString + "]: Displaying authentication screen"); + return; + }else { + if(authRequireEvent.getUseAuthType() == null) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, "IsAuthRequiredEvent was not fully handled") + .addListener(ChannelFutureListener.CLOSE); + EaglerXBungee.logger().severe("[" + localAddrString + "]: Disconnecting, no authentication method provided by handler"); + return; + } + } + continueThread.run(); + }); + + if(authConfig.isUseBuiltInAuthentication()) { + DefaultAuthSystem authSystem = eaglerXBungee.getAuthService(); + if(authSystem != null) { + authSystem.handleIsAuthRequiredEvent(authRequireEvent); + } + }else { + eaglerXBungee.getProxy().getPluginManager().callEvent(authRequireEvent); + } + + if(!authRequireEvent.isAsyncContinue()) { + authRequireEvent.doDirectContinue(); + } + }catch(Throwable t) { + throw new EventException(t); + } + }else { + continueThread.run(); + } + }else { + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + sendErrorWrong(ctx, op, "STATE_OPENED").addListener(ChannelFutureListener.CLOSE); + } + } + break; + case HandshakePacketTypes.PROTOCOL_CLIENT_REQUEST_LOGIN: { + if(clientLoginState == HandshakePacketTypes.STATE_CLIENT_VERSION) { + clientLoginState = HandshakePacketTypes.STATE_STALLING; + + int strlen = buffer.readUnsignedByte(); + clientUsername = buffer.readCharSequence(strlen, StandardCharsets.US_ASCII); + + String usrs = clientUsername.toString(); + if(!usrs.equals(usrs.replaceAll("[^A-Za-z0-9_]", "_").trim())) { + sendLoginDenied(ctx, "Invalid characters in username") + .addListener(ChannelFutureListener.CLOSE); + return; + } + + if(clientUsername.length() < 3) { + sendLoginDenied(ctx, "Username must be at least 3 characters") + .addListener(ChannelFutureListener.CLOSE); + return; + } + + if(clientUsername.length() > 16) { + sendLoginDenied(ctx, "Username must be under 16 characters") + .addListener(ChannelFutureListener.CLOSE); + return; + } + + if(clientAuthUsername == null) { + clientAuthUsername = new byte[strlen]; + for(int i = 0; i < strlen; ++i) { + clientAuthUsername[i] = (byte)clientUsername.charAt(i); + } + } + + String offlinePlayerStr = "OfflinePlayer:"; + byte[] uuidHashGenerator = new byte[offlinePlayerStr.length() + clientAuthUsername.length]; + System.arraycopy(offlinePlayerStr.getBytes(StandardCharsets.US_ASCII), 0, uuidHashGenerator, 0, offlinePlayerStr.length()); + System.arraycopy(clientAuthUsername, 0, uuidHashGenerator, offlinePlayerStr.length(), clientAuthUsername.length); + clientUUID = UUID.nameUUIDFromBytes(uuidHashGenerator); + + strlen = buffer.readUnsignedByte(); + clientRequestedServer = buffer.readCharSequence(strlen, StandardCharsets.US_ASCII); + strlen = buffer.readUnsignedByte(); + clientAuthPassword = new byte[strlen]; + buffer.readBytes(clientAuthPassword); + + if(buffer.isReadable()) { + throw new IllegalArgumentException("Packet too long"); + } + + Runnable continueThread = () -> { + + final BungeeCord bungee = BungeeCord.getInstance(); + String usernameStr = clientUsername.toString(); + final ProxiedPlayer oldName = bungee.getPlayer(usernameStr); + if (oldName != null) { + sendLoginDenied(ctx, bungee.getTranslation("already_connected_proxy", new Object[0])) + .addListener(ChannelFutureListener.CLOSE); + return; + } + + clientLoginState = HandshakePacketTypes.STATE_CLIENT_LOGIN; + ByteBuf buf = Unpooled.buffer(); + buf.writeByte(HandshakePacketTypes.PROTOCOL_SERVER_ALLOW_LOGIN); + buf.writeByte(clientUsername.length()); + buf.writeCharSequence(clientUsername, StandardCharsets.US_ASCII); + buf.writeLong(clientUUID.getMostSignificantBits()); + buf.writeLong(clientUUID.getLeastSignificantBits()); + ctx.writeAndFlush(new BinaryWebSocketFrame(buf)); + }; + + EaglerXBungee eaglerXBungee = EaglerXBungee.getEagler(); + EaglerAuthConfig authConfig = eaglerXBungee.getConfig().getAuthConfig(); + + if(authConfig.isEnableAuthentication() && clientAuth) { + if(clientAuthPassword.length == 0) { + sendLoginDenied(ctx, "Client provided no authentication code") + .addListener(ChannelFutureListener.CLOSE); + return; + }else { + try { + EaglercraftHandleAuthPasswordEvent handleEvent = new EaglercraftHandleAuthPasswordEvent( + conf, remoteAddress, authRequireEvent.getOriginHeader(), clientAuthUsername, + authRequireEvent.getSaltingData(), clientUsername, clientUUID, clientAuthPassword, + authRequireEvent.getUseAuthType(), authRequireEvent.getAuthMessage(), + (Object) authRequireEvent.getAuthAttachment(), clientRequestedServer.toString(), + (handleAuthEvent) -> { + + if(handleAuthEvent.getLoginAllowed() != EaglercraftHandleAuthPasswordEvent.AuthResponse.ALLOW) { + sendLoginDenied(ctx, handleAuthEvent.getLoginDeniedMessage()).addListener(ChannelFutureListener.CLOSE); + return; + } + + clientUsername = handleAuthEvent.getProfileUsername(); + clientUUID = handleAuthEvent.getProfileUUID(); + + String texPropOverrideValue = handleAuthEvent.getApplyTexturesPropertyValue(); + if(texPropOverrideValue != null) { + String texPropOverrideSig = handleAuthEvent.getApplyTexturesPropertySignature(); + texturesOverrideProperty = new Property("textures", texPropOverrideValue, texPropOverrideSig); + } + + overrideEaglerToVanillaSkins = handleAuthEvent.isOverrideEaglerToVanillaSkins(); + + continueThread.run(); + }); + + if(authConfig.isUseBuiltInAuthentication()) { + DefaultAuthSystem authSystem = eaglerXBungee.getAuthService(); + if(authSystem != null) { + authSystem.handleAuthPasswordEvent(handleEvent); + } + }else { + eaglerXBungee.getProxy().getPluginManager().callEvent(handleEvent); + } + + if(!handleEvent.isAsyncContinue()) { + handleEvent.doDirectContinue(); + } + }catch(Throwable t) { + throw new EventException(t); + } + } + }else { + continueThread.run(); + } + + }else { + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + sendErrorWrong(ctx, op, "STATE_CLIENT_VERSION") + .addListener(ChannelFutureListener.CLOSE); + } + } + break; + case HandshakePacketTypes.PROTOCOL_CLIENT_PROFILE_DATA: { + if(clientLoginState == HandshakePacketTypes.STATE_CLIENT_LOGIN) { + + if(profileData.size() > 12) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_EXCESSIVE_PROFILE_DATA, "Too many profile data packets recieved") + .addListener(ChannelFutureListener.CLOSE); + return; + } + + int strlen = buffer.readUnsignedByte(); + String dataType = buffer.readCharSequence(strlen, StandardCharsets.US_ASCII).toString(); + strlen = buffer.readUnsignedShort(); + byte[] readData = new byte[strlen]; + buffer.readBytes(readData); + + if(buffer.isReadable()) { + throw new IllegalArgumentException("Packet too long"); + } + + if(!profileData.containsKey(dataType)) { + profileData.put(dataType, readData); + }else { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_DUPLICATE_PROFILE_DATA, "Multiple profile data packets of the same type recieved") + .addListener(ChannelFutureListener.CLOSE); + return; + } + + }else { + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + sendErrorWrong(ctx, op, "STATE_CLIENT_LOGIN").addListener(ChannelFutureListener.CLOSE); + } + } + break; + case HandshakePacketTypes.PROTOCOL_CLIENT_FINISH_LOGIN: { + if(clientLoginState == HandshakePacketTypes.STATE_CLIENT_LOGIN) { + clientLoginState = HandshakePacketTypes.STATE_STALLING; + if(buffer.isReadable()) { + throw new IllegalArgumentException("Packet too long"); + } + + finish(ctx); + + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + }else { + sendErrorWrong(ctx, op, "STATE_CLIENT_LOGIN").addListener(ChannelFutureListener.CLOSE); + } + } + break; + default: + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_UNKNOWN_PACKET, "Unknown Packet #" + op) + .addListener(ChannelFutureListener.CLOSE); + break; + } + }catch(Throwable ex) { + if(ex instanceof EventException) { + EaglerXBungee.logger().log(Level.SEVERE, "[" + localAddrString + "]: Hanshake packet " + op + " caught an exception", ex.getCause()); + } + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_INVALID_PACKET, op == -1 ? + "Invalid Packet" : "Invalid Packet #" + op) + .addListener(ChannelFutureListener.CLOSE); + } + } + + private void finish(final ChannelHandlerContext ctx) { + final BungeeCord bungee = BungeeCord.getInstance(); + + // verify player counts a second time after handshake just to be safe + int limit = bungee.config.getPlayerLimit(); + if (limit > 0 && bungee.getOnlineCount() >= limit) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, bungee.getTranslation("proxy_full")) + .addListener(ChannelFutureListener.CLOSE); + connectionClosed = true; + return; + } + + if(conf.getMaxPlayer() > 0) { + int i = 0; + for(ProxiedPlayer p : bungee.getPlayers()) { + if(p.getPendingConnection().getListener() == conf) { + ++i; + } + } + + if (i >= conf.getMaxPlayer()) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, bungee.getTranslation("proxy_full")) + .addListener(ChannelFutureListener.CLOSE); + connectionClosed = true; + return; + } + } + + final String usernameStr = clientUsername.toString(); + final ProxiedPlayer oldName = bungee.getPlayer(usernameStr); + if (oldName != null) { + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, + bungee.getTranslation("already_connected_proxy", new Object[0])) + .addListener(ChannelFutureListener.CLOSE); + return; + } + final ChannelWrapper ch = new EaglerChannelWrapper(ctx); + InetSocketAddress baseAddress = (InetSocketAddress)ctx.channel().remoteAddress(); + InetAddress addr = ctx.channel().attr(EaglerPipeline.REAL_ADDRESS).get(); + if(addr != null) { + baseAddress = new InetSocketAddress(addr, baseAddress.getPort()); + ch.setRemoteAddress(baseAddress); + } + EaglerUpdateConfig updateconf = EaglerXBungee.getEagler().getConfig().getUpdateConfig(); + boolean blockUpdate = updateconf.isBlockAllClientUpdates(); + ClientCertificateHolder cert = null; + if(!blockUpdate && !updateconf.isDiscardLoginPacketCerts()) { + byte[] b = profileData.get("update_cert_v1"); + if(b != null && b.length < 32759) { + EaglerUpdateSvc.sendCertificateToPlayers(EaglerUpdateSvc.tryMakeHolder(b)); + } + } + final EaglerInitialHandler initialHandler = new EaglerInitialHandler(bungee, conf, ch, gameProtocolVersion, + usernameStr, clientUUID, baseAddress, ctx.channel().attr(EaglerPipeline.HOST).get(), + ctx.channel().attr(EaglerPipeline.ORIGIN).get(), cert); + if(!blockUpdate) { + List set = EaglerUpdateSvc.getCertList(); + synchronized(set) { + initialHandler.certificatesToSend.addAll(set); + } + for(ProxiedPlayer p : bungee.getPlayers()) { + if(p.getPendingConnection() instanceof EaglerInitialHandler) { + EaglerInitialHandler pp = (EaglerInitialHandler)p.getPendingConnection(); + if(pp.clientCertificate != null && pp.clientCertificate != cert) { + initialHandler.certificatesToSend.add(pp.clientCertificate); + } + } + } + } + final Callback complete = (Callback) new Callback() { + public void done(final LoginEvent result, final Throwable error) { + if (result.isCancelled()) { + final BaseComponent[] reason = result.getCancelReasonComponents(); + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, + ComponentSerializer.toString(reason != null ? reason + : TextComponent.fromLegacyText(bungee.getTranslation("kick_message", new Object[0])))) + .addListener(ChannelFutureListener.CLOSE); + return; + } + if (!ctx.channel().isActive()) { + return; + } + + ByteBuf buf = Unpooled.buffer(); + buf.writeByte(HandshakePacketTypes.PROTOCOL_SERVER_FINISH_LOGIN); + ctx.writeAndFlush(new BinaryWebSocketFrame(buf)).addListener(new GenericFutureListener>() { + + @Override + public void operationComplete(Future var1) throws Exception { + EaglerConnectionInstance eaglerCon = ctx.channel().attr(EaglerPipeline.CONNECTION_INSTANCE).get(); + + EaglerXBungee.logger().info("[" + ctx.channel().remoteAddress() + "]: Logged in as '" + usernameStr + "'"); + + final UserConnection userCon = eaglerCon.userConnection = new UserConnection(bungee, ch, usernameStr, initialHandler); + userCon.setCompressionThreshold(-1); + try { + if (!userCon.init()) { + userCon.disconnect(bungee.getTranslation("already_connected_proxy")); + EaglerPipeline.closeChannel(ctx.channel()); + return; + } + } catch (NoSuchMethodError e) { + UserConnection.class.getDeclaredMethod("init").invoke(userCon); + } + + ChannelPipeline pp = ctx.channel().pipeline(); + + HandlerBoss handler = new HandlerBoss() { + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + EaglerPipeline.closeChannel(ctx.channel()); + } + }; + handler.setHandler(new UpstreamBridge(bungee, userCon)); + try { + handler.channelActive(ctx); + } catch (Exception e) { + } + + pp.replace(HttpWebSocketHandler.this, "HandlerBoss", handler); + + pp.addBefore("HandlerBoss", "ReadTimeoutHandler", new ReadTimeoutHandler((BungeeCord.getInstance()).config.getTimeout(), TimeUnit.MILLISECONDS)); + + pp.addBefore("HandlerBoss", "EaglerMinecraftDecoder", new EaglerMinecraftDecoder( + EaglerBungeeProtocol.GAME, false, gameProtocolVersion)); + + pp.addBefore("HandlerBoss", "EaglerMinecraftByteBufEncoder", new EaglerMinecraftByteBufEncoder()); + + pp.addBefore("HandlerBoss", "EaglerMinecraftWrappedEncoder", new EaglerMinecraftWrappedEncoder()); + + pp.addBefore("HandlerBoss", "EaglerMinecraftEncoder", new EaglerMinecraftEncoder( + EaglerBungeeProtocol.GAME, true, gameProtocolVersion)); + + boolean doRegisterSkins = true; + + EaglercraftRegisterSkinEvent registerSkinEvent = new EaglercraftRegisterSkinEvent(usernameStr, clientUUID); + + bungee.getPluginManager().callEvent(registerSkinEvent); + + Property prop = registerSkinEvent.getForceUseMojangProfileProperty(); + boolean useExistingProp = registerSkinEvent.getForceUseLoginResultObjectTextures(); + if(prop != null) { + texturesOverrideProperty = prop; + overrideEaglerToVanillaSkins = true; + }else { + if(useExistingProp) { + overrideEaglerToVanillaSkins = true; + }else { + byte[] custom = registerSkinEvent.getForceSetUseCustomPacket(); + if(custom != null) { + profileData.put("skin_v1", custom); + overrideEaglerToVanillaSkins = false; + }else { + String customUrl = registerSkinEvent.getForceSetUseURL(); + if(customUrl != null) { + EaglerXBungee.getEagler().getSkinService().registerTextureToPlayerAssociation(customUrl, initialHandler.getUniqueId()); + doRegisterSkins = false; + overrideEaglerToVanillaSkins = false; + } + } + } + } + + EaglerBungeeConfig eaglerConf = EaglerXBungee.getEagler().getConfig(); + + + if(texturesOverrideProperty != null) { + LoginResult oldProfile = initialHandler.getLoginProfile(); + if(oldProfile == null) { + oldProfile = new LoginResult(initialHandler.getUniqueId().toString(), initialHandler.getName(), null); + initialHandler.setLoginProfile(oldProfile); + } + oldProfile.setProperties(new Property[] { texturesOverrideProperty, EaglerBungeeConfig.isEaglerProperty }); + }else { + if(!useExistingProp) { + String vanillaSkin = eaglerConf.getEaglerPlayersVanillaSkin(); + if(vanillaSkin != null) { + LoginResult oldProfile = initialHandler.getLoginProfile(); + if(oldProfile == null) { + oldProfile = new LoginResult(initialHandler.getUniqueId().toString(), initialHandler.getName(), null); + initialHandler.setLoginProfile(oldProfile); + } + oldProfile.setProperties(eaglerConf.getEaglerPlayersVanillaSkinProperties()); + } + } + } + + if(overrideEaglerToVanillaSkins) { + LoginResult res = initialHandler.getLoginProfile(); + if(res != null) { + Property[] props = res.getProperties(); + if(props != null) { + for(int i = 0; i < props.length; ++i) { + if("textures".equals(props[i].getName())) { + try { + String jsonStr = SkinPackets.bytesToAscii(Base64.decodeBase64(props[i].getValue())); + JsonObject json = (new JsonParser()).parse(jsonStr).getAsJsonObject(); + JsonObject skinObj = json.getAsJsonObject("SKIN"); + if(skinObj != null) { + JsonElement url = json.get("url"); + if(url != null) { + String urlStr = SkinService.sanitizeTextureURL(url.getAsString()); + EaglerXBungee.getEagler().getSkinService().registerTextureToPlayerAssociation(urlStr, initialHandler.getUniqueId()); + } + } + doRegisterSkins = false; + }catch(Throwable t) { + } + break; + } + } + } + } + } + + if(doRegisterSkins) { + if(profileData.containsKey("skin_v1")) { + try { + SkinPackets.registerEaglerPlayer(clientUUID, profileData.get("skin_v1"), + EaglerXBungee.getEagler().getSkinService()); + } catch (Throwable ex) { + SkinPackets.registerEaglerPlayerFallback(clientUUID, EaglerXBungee.getEagler().getSkinService()); + EaglerXBungee.logger().info("[" + ctx.channel().remoteAddress() + "]: Invalid skin packet: " + ex.toString()); + } + }else { + SkinPackets.registerEaglerPlayerFallback(clientUUID, EaglerXBungee.getEagler().getSkinService()); + } + } + + if(profileData.containsKey("cape_v1")) { + try { + CapePackets.registerEaglerPlayer(clientUUID, profileData.get("cape_v1"), + EaglerXBungee.getEagler().getCapeService()); + } catch (Throwable ex) { + CapePackets.registerEaglerPlayerFallback(clientUUID, EaglerXBungee.getEagler().getCapeService()); + EaglerXBungee.logger().info("[" + ctx.channel().remoteAddress() + "]: Invalid cape packet: " + ex.toString()); + } + }else { + CapePackets.registerEaglerPlayerFallback(clientUUID, EaglerXBungee.getEagler().getCapeService()); + } + + if(conf.getEnableVoiceChat()) { + EaglerXBungee.getEagler().getVoiceService().handlePlayerLoggedIn(userCon); + } + + ServerInfo server; + if (bungee.getReconnectHandler() != null) { + server = bungee.getReconnectHandler().getServer((ProxiedPlayer) userCon); + } else { + server = AbstractReconnectHandler.getForcedHost(initialHandler); + } + + if (server == null) { + server = bungee.getServerInfo(conf.getDefaultServer()); + } + + final ServerInfo server2 = server; + Callback complete = new Callback() { + @Override + public void done(PostLoginEvent result, Throwable error) { + eaglerCon.hasBeenForwarded = true; + if (ch.isClosed()) { + return; + } + userCon.connect(server2, null, true, ServerConnectEvent.Reason.JOIN_PROXY); + } + }; + + try { + PostLoginEvent login = new PostLoginEvent(userCon); + bungee.getPluginManager().callEvent(login); + complete.done(login, null); + }catch(NoSuchMethodError err) { + bungee.getPluginManager().callEvent(PostLoginEvent.class.getDeclaredConstructor(ProxiedPlayer.class, ServerInfo.class, Callback.class).newInstance(userCon, server, complete)); + } + } + + }); + } + }; + final Callback completePre = new Callback() { + public void done(PreLoginEvent var1, Throwable var2) { + if (var1.isCancelled()) { + final BaseComponent[] reason = var1.getCancelReasonComponents(); + sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE, + ComponentSerializer.toString(reason != null ? reason + : TextComponent.fromLegacyText(bungee.getTranslation("kick_message", new Object[0])))) + .addListener(ChannelFutureListener.CLOSE); + }else { + bungee.getPluginManager().callEvent(new LoginEvent(initialHandler, complete)); + } + } + }; + bungee.getPluginManager().callEvent(new PreLoginEvent(initialHandler, completePre)); + } + + private static class EventException extends RuntimeException { + public EventException(Throwable t) { + super(t.toString(), t); + } + } + + private void handleText(ChannelHandlerContext ctx, String str) { + if(connectionClosed) { + return; + } + if(!hasFirstPacket && (conf.isAllowMOTD() || conf.isAllowQuery()) && (str = str.toLowerCase()).startsWith("accept:")) { + str = str.substring(7).trim(); + hasFirstPacket = true; + hasBinaryConnection = false; + + if(CommandConfirmCode.confirmHash != null && str.equalsIgnoreCase(CommandConfirmCode.confirmHash)) { + ctx.writeAndFlush(new TextWebSocketFrame("OK")).addListener(ChannelFutureListener.CLOSE); + CommandConfirmCode.confirmHash = null; + connectionClosed = true; + return; + } + + boolean isMOTD = str.startsWith("motd"); + + SocketAddress localSocketAddr = ctx.channel().remoteAddress(); + InetAddress addr = ctx.channel().attr(EaglerPipeline.REAL_ADDRESS).get(); + + String limiterAddress = null; + RateLimitStatus queryRateLimit = RateLimitStatus.OK; + if(addr != null) { + limiterAddress = addr.getHostAddress(); + }else { + if(localSocketAddr instanceof InetSocketAddress) { + limiterAddress = ((InetSocketAddress)localSocketAddr).getAddress().getHostAddress(); + } + } + + EaglerRateLimiter limiter = isMOTD ? conf.getRatelimitMOTD() : conf.getRatelimitQuery(); + if(limiterAddress != null && limiter != null) { + queryRateLimit = limiter.rateLimit(limiterAddress); + } + + if(queryRateLimit == RateLimitStatus.LOCKED_OUT) { + ctx.close(); + connectionClosed = true; + return; + } + + if(queryRateLimit != RateLimitStatus.OK) { + final RateLimitStatus rateLimitTypeFinal = queryRateLimit; + ctx.writeAndFlush(new TextWebSocketFrame( + rateLimitTypeFinal == RateLimitStatus.LIMITED_NOW_LOCKED_OUT ? "{\"type\":\"locked\"}" : "{\"type\":\"blocked\"}")) + .addListener(ChannelFutureListener.CLOSE); + connectionClosed = true; + return; + } + + HttpServerQueryHandler handler = null; + if(isMOTD) { + if(conf.isAllowMOTD()) { + handler = new MOTDQueryHandler(); + } + }else if(conf.isAllowQuery()) { + handler = QueryManager.createQueryHandler(str); + } + if(handler != null) { + ctx.pipeline().replace(HttpWebSocketHandler.this, "HttpServerQueryHandler", handler); + ctx.pipeline().addBefore("HttpServerQueryHandler", "WriteTimeoutHandler", new WriteTimeoutHandler(5l, TimeUnit.SECONDS)); + handler.beginHandleQuery(conf, ctx, str); + if(handler instanceof MOTDQueryHandler) { + EaglercraftMOTDEvent evt = new EaglercraftMOTDEvent((MOTDQueryHandler)handler); + BungeeCord.getInstance().getPluginManager().callEvent(evt); + if(!handler.isClosed()) { + ((MOTDQueryHandler)handler).sendToUser(); + } + } + if(!handler.isClosed() && !handler.shouldKeepAlive()) { + connectionClosed = true; + handler.close(); + } + }else { + connectionClosed = true; + ctx.close(); + } + }else { + connectionClosed = true; + ctx.close(); + return; + } + } + + private int getAuthMethodId(AuthMethod meth) { + switch(meth) { + case PLAINTEXT: + return 255; // plaintext authentication + case EAGLER_SHA256: + return 1; // eagler_sha256 authentication + case AUTHME_SHA256: + return 2; // authme_sha256 authentication + default: + return -1; + } + } + + private ChannelFuture sendLoginDenied(ChannelHandlerContext ctx, String reason) { + if((!isProtocolExchanged || clientProtocolVersion == 2) && reason.length() > 255) { + reason = reason.substring(0, 256); + }else if(reason.length() > 65535) { + reason = reason.substring(0, 65536); + } + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + connectionClosed = true; + ByteBuf buf = Unpooled.buffer(); + buf.writeByte(HandshakePacketTypes.PROTOCOL_SERVER_DENY_LOGIN); + byte[] msg = reason.getBytes(StandardCharsets.UTF_8); + if(!isProtocolExchanged || clientProtocolVersion == 2) { + buf.writeByte(msg.length); + }else { + buf.writeShort(msg.length); + } + buf.writeBytes(msg); + return ctx.writeAndFlush(new BinaryWebSocketFrame(buf)); + } + + private ChannelFuture sendErrorWrong(ChannelHandlerContext ctx, int op, String state) { + return sendErrorCode(ctx, HandshakePacketTypes.SERVER_ERROR_WRONG_PACKET, "Wrong Packet #" + op + " in state '" + state + "'"); + } + + private ChannelFuture sendErrorCode(ChannelHandlerContext ctx, int code, String str) { + if((!isProtocolExchanged || clientProtocolVersion == 2) && str.length() > 255) { + str = str.substring(0, 256); + }else if(str.length() > 65535) { + str = str.substring(0, 65536); + } + clientLoginState = HandshakePacketTypes.STATE_CLIENT_COMPLETE; + connectionClosed = true; + ByteBuf buf = Unpooled.buffer(); + buf.writeByte(HandshakePacketTypes.PROTOCOL_SERVER_ERROR); + buf.writeByte(code); + byte[] msg = str.getBytes(StandardCharsets.UTF_8); + if(!isProtocolExchanged || clientProtocolVersion == 2) { + buf.writeByte(msg.length); + }else { + buf.writeShort(msg.length); + } + buf.writeBytes(msg); + return ctx.writeAndFlush(new BinaryWebSocketFrame(buf)); + } + + public void channelInactive(ChannelHandlerContext ctx) { + connectionClosed = true; + EaglerPipeline.closeChannel(ctx.channel()); + } + + private void handleLegacyClient(ChannelHandlerContext ctx, ByteBuf buffer) { + connectionClosed = true; + ByteBuf kickMsg = ctx.alloc().buffer(); + final String redir = conf.redirectLegacyClientsTo(); + if(redir != null) { + writeLegacyRedirect(kickMsg, redir); + }else { + writeLegacyKick(kickMsg, "This is an EaglercraftX 1.8 server, it is not compatible with 1.5.2!"); + } + ctx.writeAndFlush(new BinaryWebSocketFrame(kickMsg)).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture var1) throws Exception { + ctx.channel().eventLoop().schedule(new Runnable() { + @Override + public void run() { + ctx.close(); + } + }, redir != null ? 100l : 500l, TimeUnit.MILLISECONDS); + } + }); + } + + public static void writeLegacyKick(ByteBuf buffer, String message) { + buffer.writeByte(0xFF); + buffer.writeShort(message.length()); + for(int i = 0, l = message.length(), j; i < l; ++i) { + j = message.charAt(i); + buffer.writeByte((j >> 8) & 0xFF); + buffer.writeByte(j & 0xFF); + } + } + + public static void writeLegacyRedirect(ByteBuf buffer, String redirect) { + buffer.writeBytes(legacyRedirectHeader); + byte[] redirect_ = redirect.getBytes(StandardCharsets.UTF_8); + buffer.writeByte((redirect_.length >> 8) & 0xFF); + buffer.writeByte(redirect_.length & 0xFF); + buffer.writeBytes(redirect_); + } + + private static final byte[] legacyRedirectHeader; + + static { + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bao); + try { + // Packet1Login + dos.writeByte(0x01); + dos.writeInt(0); + dos.writeShort(0); + dos.writeByte(0); + dos.writeByte(0); + dos.writeByte(0xFF); + dos.writeByte(0); + dos.writeByte(0); + // Packet250CustomPayload + dos.writeByte(0xFA); + String channel = "EAG|Reconnect"; + int cl = channel.length(); + dos.writeShort(cl); + for(int i = 0; i < cl; ++i) { + dos.writeChar(channel.charAt(i)); + } + }catch(IOException ex) { + throw new ExceptionInInitializerError(ex); + } + legacyRedirectHeader = bao.toByteArray(); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/bungeeprotocol/EaglerBungeeProtocol.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/bungeeprotocol/EaglerBungeeProtocol.java new file mode 100644 index 0000000..0d61142 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/bungeeprotocol/EaglerBungeeProtocol.java @@ -0,0 +1,254 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.bungeeprotocol; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import gnu.trove.map.TIntObjectMap; +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TIntObjectHashMap; +import gnu.trove.map.hash.TObjectIntHashMap; +import net.md_5.bungee.protocol.BadPacketException; +import net.md_5.bungee.protocol.DefinedPacket; +import net.md_5.bungee.protocol.ProtocolConstants; +import net.md_5.bungee.protocol.packet.*; + +import java.util.function.Supplier; + +/** + * The original net.md_5.bungee.protocol.Protocol is inaccessible due to java + * security rules + */ + +public enum EaglerBungeeProtocol { + + // Undef + HANDSHAKE { + + { + TO_SERVER.registerPacket(Handshake.class, Handshake::new, map(ProtocolConstants.MINECRAFT_1_8, 0x00)); + } + }, + // 0 + GAME { + + { + TO_CLIENT.registerPacket(KeepAlive.class, KeepAlive::new, map(ProtocolConstants.MINECRAFT_1_8, 0x00)); + TO_CLIENT.registerPacket(Login.class, Login::new, map(ProtocolConstants.MINECRAFT_1_8, 0x01)); + TO_CLIENT.registerPacket(Chat.class, Chat::new, map(ProtocolConstants.MINECRAFT_1_8, 0x02)); + TO_CLIENT.registerPacket(Respawn.class, Respawn::new, map(ProtocolConstants.MINECRAFT_1_8, 0x07)); + TO_CLIENT.registerPacket(PlayerListItem.class, // PlayerInfo + PlayerListItem::new, map(ProtocolConstants.MINECRAFT_1_8, 0x38)); + TO_CLIENT.registerPacket(TabCompleteResponse.class, TabCompleteResponse::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x3A)); + TO_CLIENT.registerPacket(ScoreboardObjective.class, ScoreboardObjective::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x3B)); + TO_CLIENT.registerPacket(ScoreboardScore.class, ScoreboardScore::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x3C)); + TO_CLIENT.registerPacket(ScoreboardDisplay.class, ScoreboardDisplay::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x3D)); + TO_CLIENT.registerPacket(Team.class, Team::new, map(ProtocolConstants.MINECRAFT_1_8, 0x3E)); + TO_CLIENT.registerPacket(PluginMessage.class, PluginMessage::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x3F)); + TO_CLIENT.registerPacket(Kick.class, Kick::new, map(ProtocolConstants.MINECRAFT_1_8, 0x40)); + TO_CLIENT.registerPacket(Title.class, Title::new, map(ProtocolConstants.MINECRAFT_1_8, 0x45)); + TO_CLIENT.registerPacket(PlayerListHeaderFooter.class, PlayerListHeaderFooter::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x47)); + TO_CLIENT.registerPacket(EntityStatus.class, EntityStatus::new, map(ProtocolConstants.MINECRAFT_1_8, 0x1A)); + + TO_SERVER.registerPacket(KeepAlive.class, KeepAlive::new, map(ProtocolConstants.MINECRAFT_1_8, 0x00)); + TO_SERVER.registerPacket(Chat.class, Chat::new, map(ProtocolConstants.MINECRAFT_1_8, 0x01)); + TO_SERVER.registerPacket(TabCompleteRequest.class, TabCompleteRequest::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x14)); + TO_SERVER.registerPacket(ClientSettings.class, ClientSettings::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x15)); + TO_SERVER.registerPacket(PluginMessage.class, PluginMessage::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x17)); + } + }, + // 1 + STATUS { + + { + TO_CLIENT.registerPacket(StatusResponse.class, StatusResponse::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x00)); + TO_CLIENT.registerPacket(PingPacket.class, PingPacket::new, map(ProtocolConstants.MINECRAFT_1_8, 0x01)); + + TO_SERVER.registerPacket(StatusRequest.class, StatusRequest::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x00)); + TO_SERVER.registerPacket(PingPacket.class, PingPacket::new, map(ProtocolConstants.MINECRAFT_1_8, 0x01)); + } + }, + // 2 + LOGIN { + + { + TO_CLIENT.registerPacket(Kick.class, Kick::new, map(ProtocolConstants.MINECRAFT_1_8, 0x00)); + TO_CLIENT.registerPacket(EncryptionRequest.class, EncryptionRequest::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x01)); + TO_CLIENT.registerPacket(LoginSuccess.class, LoginSuccess::new, map(ProtocolConstants.MINECRAFT_1_8, 0x02)); + TO_CLIENT.registerPacket(SetCompression.class, SetCompression::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x03)); + + TO_SERVER.registerPacket(LoginRequest.class, LoginRequest::new, map(ProtocolConstants.MINECRAFT_1_8, 0x00)); + TO_SERVER.registerPacket(EncryptionResponse.class, EncryptionResponse::new, + map(ProtocolConstants.MINECRAFT_1_8, 0x01)); + } + }, + // 3 + CONFIGURATION { + + }; + + /* ======================================================================== */ + public static final int MAX_PACKET_ID = 0xFF; + /* ======================================================================== */ + public final DirectionData TO_SERVER = new DirectionData(this, ProtocolConstants.Direction.TO_SERVER); + public final DirectionData TO_CLIENT = new DirectionData(this, ProtocolConstants.Direction.TO_CLIENT); + + public static void main(String[] args) { + for (int version : ProtocolConstants.SUPPORTED_VERSION_IDS) { + dump(version); + } + } + + private static void dump(int version) { + for (EaglerBungeeProtocol protocol : EaglerBungeeProtocol.values()) { + dump(version, protocol); + } + } + + private static void dump(int version, EaglerBungeeProtocol protocol) { + dump(version, protocol.TO_CLIENT); + dump(version, protocol.TO_SERVER); + } + + private static void dump(int version, DirectionData data) { + for (int id = 0; id < MAX_PACKET_ID; id++) { + DefinedPacket packet = data.createPacket(id, version); + if (packet != null) { + System.out.println(version + " " + data.protocolPhase + " " + data.direction + " " + id + " " + + packet.getClass().getSimpleName()); + } + } + } + + private static class ProtocolData { + + private final int protocolVersion; + private final TObjectIntMap> packetMap = new TObjectIntHashMap<>(MAX_PACKET_ID); + @SuppressWarnings("unchecked") + private final Supplier[] packetConstructors = new Supplier[MAX_PACKET_ID]; + + private ProtocolData(int protocolVersion) { + this.protocolVersion = protocolVersion; + } + } + + private static class ProtocolMapping { + + private final int protocolVersion; + private final int packetID; + + private ProtocolMapping(int protocolVersion, int packetID) { + this.protocolVersion = protocolVersion; + this.packetID = packetID; + } + } + + // Helper method + private static ProtocolMapping map(int protocol, int id) { + return new ProtocolMapping(protocol, id); + } + + public static final class DirectionData { + + private final TIntObjectMap protocols = new TIntObjectHashMap<>(); + // + private final EaglerBungeeProtocol protocolPhase; + private final ProtocolConstants.Direction direction; + + public DirectionData(EaglerBungeeProtocol protocolPhase, ProtocolConstants.Direction direction) { + this.protocolPhase = protocolPhase; + this.direction = direction; + + for (int protocol : ProtocolConstants.SUPPORTED_VERSION_IDS) { + protocols.put(protocol, new ProtocolData(protocol)); + } + } + + private ProtocolData getProtocolData(int version) { + ProtocolData protocol = protocols.get(version); + if (protocol == null && (protocolPhase != EaglerBungeeProtocol.GAME)) { + protocol = Iterables.getFirst(protocols.valueCollection(), null); + } + return protocol; + } + + public final DefinedPacket createPacket(int id, int version) { + ProtocolData protocolData = getProtocolData(version); + if (protocolData == null) { + throw new BadPacketException("Unsupported protocol version " + version); + } + if (id > MAX_PACKET_ID || id < 0) { + throw new BadPacketException("Packet with id " + id + " outside of range"); + } + + Supplier constructor = protocolData.packetConstructors[id]; + return (constructor == null) ? null : constructor.get(); + } + + private void registerPacket(Class packetClass, + Supplier constructor, ProtocolMapping... mappings) { + int mappingIndex = 0; + ProtocolMapping mapping = mappings[mappingIndex]; + for (int protocol : ProtocolConstants.SUPPORTED_VERSION_IDS) { + if (protocol < mapping.protocolVersion) { + // This is a new packet, skip it till we reach the next protocol + continue; + } + + if (mapping.protocolVersion < protocol && mappingIndex + 1 < mappings.length) { + // Mapping is non current, but the next one may be ok + ProtocolMapping nextMapping = mappings[mappingIndex + 1]; + + if (nextMapping.protocolVersion == protocol) { + Preconditions.checkState(nextMapping.packetID != mapping.packetID, + "Duplicate packet mapping (%s, %s)", mapping.protocolVersion, + nextMapping.protocolVersion); + + mapping = nextMapping; + mappingIndex++; + } + } + + if (mapping.packetID < 0) { + break; + } + + ProtocolData data = protocols.get(protocol); + data.packetMap.put(packetClass, mapping.packetID); + data.packetConstructors[mapping.packetID] = constructor; + } + } + + public boolean hasPacket(Class packet, int version) { + ProtocolData protocolData = getProtocolData(version); + if (protocolData == null) { + throw new BadPacketException("Unsupported protocol version"); + } + + return protocolData.packetMap.containsKey(packet); + } + + final int getId(Class packet, int version) { + + ProtocolData protocolData = getProtocolData(version); + if (protocolData == null) { + throw new BadPacketException("Unsupported protocol version"); + } + Preconditions.checkArgument(protocolData.packetMap.containsKey(packet), + "Cannot get ID for packet %s in phase %s with direction %s", packet, protocolPhase, direction); + + return protocolData.packetMap.get(packet); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/bungeeprotocol/EaglerProtocolAccessProxy.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/bungeeprotocol/EaglerProtocolAccessProxy.java new file mode 100644 index 0000000..4b1bcbc --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/bungeeprotocol/EaglerProtocolAccessProxy.java @@ -0,0 +1,32 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.bungeeprotocol; + +import net.md_5.bungee.protocol.DefinedPacket; + +/** + * 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 EaglerProtocolAccessProxy { + + public static int getPacketId(EaglerBungeeProtocol protocol, int protocolVersion, DefinedPacket pkt, boolean server) { + final EaglerBungeeProtocol.DirectionData prot = server ? protocol.TO_CLIENT : protocol.TO_SERVER; + return prot.getId((Class) pkt.getClass(), protocolVersion); + } + + public static DefinedPacket createPacket(EaglerBungeeProtocol protocol, int protocolVersion, int packetId, boolean server) { + final EaglerBungeeProtocol.DirectionData prot = server ? protocol.TO_CLIENT : protocol.TO_SERVER; + return prot.createPacket(packetId, protocolVersion); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/MOTDQueryHandler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/MOTDQueryHandler.java new file mode 100644 index 0000000..ca4651f --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/MOTDQueryHandler.java @@ -0,0 +1,228 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.query.EaglerQuerySimpleHandler; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.query.MOTDConnection; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerListenerConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.MOTDCacheConfiguration; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +/** + * 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 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(); + String[] lns = listener.getMotd().split("\n"); + if(lns.length >= 1) { + line1 = lns[0]; + } + if(lns.length >= 2) { + line2 = lns[1]; + } + maxPlayers = listener.getMaxPlayers(); + onlinePlayers = ProxyServer.getInstance().getOnlineCount(); + players = new ArrayList(); + for(ProxiedPlayer pp : ProxyServer.getInstance().getPlayers()) { + players.add(pp.getDisplayName()); + if(players.size() >= 9) { + players.add("" + ChatColor.GRAY + ChatColor.ITALIC + "(" + (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 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 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/QueryManager.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/QueryManager.java new file mode 100644 index 0000000..e8c13be --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/QueryManager.java @@ -0,0 +1,102 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.query; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; + +import com.google.gson.JsonObject; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerBungeeConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.HttpServerQueryHandler; +import net.md_5.bungee.api.plugin.PluginDescription; + +/** + * 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 QueryManager { + + private static final Map> 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 clazz; + synchronized(queryTypes) { + clazz = queryTypes.get(type); + } + if(clazz != null) { + HttpServerQueryHandler obj = null; + try { + obj = clazz.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception creating query handler for '" + type + "'!", e); + } + if(obj != null) { + return obj; + } + } + return null; + } + + public static void registerQueryType(String name, Class clazz) { + synchronized(queryTypes) { + if(queryTypes.put(name, clazz) != null) { + EaglerXBungee.logger().warning("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() { + EaglerXBungee plugin = EaglerXBungee.getEagler(); + EaglerBungeeConfig conf = plugin.getConfig(); + JsonObject json = new JsonObject(); + json.addProperty("name", conf.getServerName()); + json.addProperty("brand", "lax1dude"); + PluginDescription desc = plugin.getDescription(); + json.addProperty("vers", "EaglerXBungee/" + desc.getVersion()); + 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/VersionQueryHandler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/VersionQueryHandler.java new file mode 100644 index 0000000..a25133a --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/query/VersionQueryHandler.java @@ -0,0 +1,53 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.query; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.api.query.EaglerQuerySimpleHandler; +import net.md_5.bungee.api.ProxyServer; + +/** + * 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", ProxyServer.getInstance().getName()); + proxyInfo.addProperty("vers", ProxyServer.getInstance().getVersion()); + responseObj.add("proxyVersions", proxyInfo); + sendJsonResponseAndClose("version", responseObj); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpContentType.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpContentType.java new file mode 100644 index 0000000..cc535de --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpContentType.java @@ -0,0 +1,49 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 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 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"; + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpMemoryCache.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpMemoryCache.java new file mode 100644 index 0000000..e849a6f --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpMemoryCache.java @@ -0,0 +1,86 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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_bungeecord.EaglerXBungee; + +/** + * 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 = "EaglerXBungee/" + EaglerXBungee.getEagler().getDescription().getVersion(); + } + + public DefaultFullHttpResponse createHTTPResponse() { + return createHTTPResponse(HttpResponseStatus.OK); + } + + public DefaultFullHttpResponse createHTTPResponse(HttpResponseStatus code) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, code, Unpooled.copiedBuffer(fileData)); + 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpWebServer.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpWebServer.java new file mode 100644 index 0000000..1cef8e7 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/server/web/HttpWebServer.java @@ -0,0 +1,291 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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_bungeecord.EaglerXBungee; + +/** + * 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 contentTypes; + private final Map filesCache; + private final List 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 contentTypes, List 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 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 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() { + EaglerXBungee plugin = EaglerXBungee.getEagler(); + byte[] src = ("" + htmlEntities(plugin.getConfig().getServerName()) + "" + + "

404 Not Found


" + + "The requested resource " + + " could not be found on this server!

" + htmlEntities(plugin.getDescription().getName()) + "/" + + htmlEntities(plugin.getDescription().getVersion()) + "

").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() { + EaglerXBungee plugin = EaglerXBungee.getEagler(); + String name = htmlEntities(plugin.getConfig().getServerName()); + byte[] src = ("" + name + + "

" + + "404 'Websocket Upgrade Failure' (rip)

The URL you have requested is the physical WebSocket address of '" + name + "'

To correctly join this server, load the latest EaglercraftX 1.8 client, click the 'Direct Connect' button
on the 'Multiplayer' screen, " + + "and enter this URL as the server address

").getBytes(StandardCharsets.UTF_8); + HttpContentType htmlContentType = new HttpContentType(new HashSet(Arrays.asList("html")), "text/html", "utf-8", 14400000l); + long millis = System.currentTimeMillis(); + return new HttpMemoryCache(null, "~404", Unpooled.wrappedBuffer(src), htmlContentType, millis, millis, millis); + } + + public static String htmlEntities(String input) { + return input.replace("<", "<").replace(">", ">"); + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/shit/CompatWarning.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/shit/CompatWarning.java new file mode 100644 index 0000000..2cdff34 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/shit/CompatWarning.java @@ -0,0 +1,61 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.shit; + +import java.util.logging.Logger; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.md_5.bungee.api.ProxyServer; + +/** + * 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 CompatWarning { + + public static void displayCompatWarning() { + String stfu = System.getProperty("eaglerxbungee.stfu"); + if("true".equalsIgnoreCase(stfu)) { + return; + } + String[] compatWarnings = new String[] { + ":>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>", + ":> ", + ":> EAGLERCRAFTXBUNGEE WARNING:", + ":> ", + ":> This plugin wasn\'t tested to be \'working\'", + ":> with ANY version of BungeeCord (and forks)", + ":> apart from the versions listed below:", + ":> ", + ":> - BungeeCord: " + EaglerXBungee.NATIVE_BUNGEECORD_BUILD, + ":> - Waterfall: " + EaglerXBungee.NATIVE_WATERFALL_BUILD, + ":> ", + ":> This is not a Bukkit/Spigot plugin!", + ":> ", + ":> Use \"-Deaglerxbungee.stfu=true\" to hide", + ":> ", + ":>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>:>" + }; + + try { + Logger fuck = ProxyServer.getInstance().getLogger(); + for(int i = 0; i < compatWarnings.length; ++i) { + fuck.warning(compatWarnings[i]); + } + }catch(Throwable t) { + for(int i = 0; i < compatWarnings.length; ++i) { + System.err.println(compatWarnings[i]); + } + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/shit/MainClass.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/shit/MainClass.java new file mode 100644 index 0000000..c3304c4 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/shit/MainClass.java @@ -0,0 +1,41 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.shit; + +import java.awt.GraphicsEnvironment; + +import javax.swing.JOptionPane; + +/** + * 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 MainClass { + + public static void main(String[] args) { + System.err.println(); + System.err.println("ERROR: The EaglerXBungee 1.8 jar file is a PLUGIN intended to be used with BungeeCord!"); + System.err.println("Place this file in the \"plugins\" directory of your BungeeCord 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 EaglerXBungee 1.8 jar file is a PLUGIN intended to be used with BungeeCord!\nPlace this file in the \"plugins\" directory of your BungeeCord installation", "EaglerXBungee", JOptionPane.ERROR_MESSAGE); + } + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/AsyncSkinProvider.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/AsyncSkinProvider.java new file mode 100644 index 0000000..17932fe --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/AsyncSkinProvider.java @@ -0,0 +1,455 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 java.util.logging.Level; + +import javax.imageio.ImageIO; + +import org.apache.commons.codec.binary.Base64; + +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_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.BinaryHttpClient.Response; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.ICacheProvider.CacheLoadedProfile; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.ICacheProvider.CacheLoadedSkin; + +/** + * 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 AsyncSkinProvider { + + private static class SkinConsumerImpl implements Consumer { + + protected final Consumer responseConsumer; + + protected SkinConsumerImpl(Consumer consumer) { + this.responseConsumer = consumer; + } + + protected void doAccept(byte[] v) { + try { + responseConsumer.accept(v); + }catch(Throwable t) { + EaglerXBungee.logger().log(Level.SEVERE, "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 { + + protected final UUID skinUUID; + protected final String skinTexture; + protected final ICacheProvider cacheProvider; + protected final Consumer responseConsumer; + + protected SkinCachingConsumer(UUID skinUUID, String skinTexture, ICacheProvider cacheProvider, + Consumer 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) { + EaglerXBungee.logger().log(Level.SEVERE, "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 { + + protected final UUID uuid; + protected final Consumer responseConsumer; + + protected ProfileConsumerImpl(UUID uuid, Consumer responseConsumer) { + this.uuid = uuid; + this.responseConsumer = responseConsumer; + } + + protected void doAccept(CacheFetchedProfile v) { + try { + responseConsumer.accept(v); + }catch(Throwable t) { + EaglerXBungee.logger().log(Level.SEVERE, "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 = (new JsonParser()).parse(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 = (new JsonParser()).parse(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 { + + protected final UUID uuid; + protected final ICacheProvider cacheProvider; + protected final Consumer responseConsumer; + + protected ProfileCachingConsumer(UUID uuid, ICacheProvider cacheProvider, Consumer 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) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception thrown writing new profile to database!", t); + } + responseConsumer.accept(profile); + }else { + responseConsumer.accept(null); + } + } + + } + + private static class UsernameToUUIDConsumerImpl implements Consumer { + + protected final String username; + protected final ICacheProvider cacheProvider; + protected final Consumer responseConsumer; + + protected UsernameToUUIDConsumerImpl(String username, ICacheProvider cacheProvider, Consumer responseConsumer) { + this.username = username; + this.cacheProvider = cacheProvider; + this.responseConsumer = responseConsumer; + } + + protected void doAccept(CacheFetchedProfile v) { + try { + responseConsumer.accept(v); + }catch(Throwable t) { + EaglerXBungee.logger().log(Level.SEVERE, "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 = (new JsonParser()).parse(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 responseConsumer) { + downloadSkin(SkinPackets.createEaglerURLSkinUUID(skinTexture), skinTexture, cacheProvider, responseConsumer); + } + + public static void downloadSkin(UUID skinUUID, String skinTexture, ICacheProvider cacheProvider, Consumer 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) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception thrown handling invalid skin!", t); + } + throw new CancelException(); + } + int globalRatelimit = EaglerXBungee.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 { + EaglerXBungee.logger().warning("skin system reached the global texture download ratelimit of " + globalRatelimit + " while downloading up \"" + skinTexture + "\""); + throw new CancelException(); + } + }else { + try { + responseConsumer.accept(loadedSkin.texture); + }catch(Throwable t) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception thrown processing cached skin!", t); + } + throw new CancelException(); + } + } + + public static void lookupProfileByUUID(UUID playerUUID, ICacheProvider cacheProvider, Consumer responseConsumer) { + lookupProfileByUUID(playerUUID, cacheProvider, responseConsumer, true); + } + + private static void lookupProfileByUUID(UUID playerUUID, ICacheProvider cacheProvider, Consumer 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 = EaglerXBungee.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 { + EaglerXBungee.logger().warning("skin system reached the global UUID lookup ratelimit of " + globalRatelimit + " while looking up \"" + playerUUID + "\""); + throw new CancelException(); + } + }else { + try { + responseConsumer.accept(new CacheFetchedProfile(profile)); + }catch(Throwable t) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception thrown processing cached profile!", t); + } + throw new CancelException(); + } + } + + public static void lookupProfileByUsername(String playerUsername, ICacheProvider cacheProvider, Consumer 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) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception thrown processing invalid profile!", t); + } + throw new CancelException(); + } + URI requestURI = URI.create("https://api.mojang.com/users/profiles/minecraft/" + playerUsername); + int globalRatelimit = EaglerXBungee.getEagler().getConfig().getUuidRateLimitGlobal(); + boolean isRateLimit; + synchronized(rateLimitLookup) { + isRateLimit = !rateLimitLookup.rateLimit(globalRatelimit); + } + if(!isRateLimit) { + BinaryHttpClient.asyncRequest("GET", requestURI, new UsernameToUUIDConsumerImpl(playerUsername, cacheProvider, responseConsumer)); + }else { + EaglerXBungee.logger().warning("skin system reached the global UUID lookup ratelimit of " + globalRatelimit + " while looking up \"" + playerUsername + "\""); + throw new CancelException(); + } + }else { + try { + responseConsumer.accept(new CacheFetchedProfile(profile)); + }catch(Throwable t) { + EaglerXBungee.logger().log(Level.SEVERE, "Exception thrown processing cached profile!", t); + } + throw new CancelException(); + } + } + + public static class CancelException extends RuntimeException { + + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/BinaryHttpClient.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/BinaryHttpClient.java new file mode 100644 index 0000000..a9e980c --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/BinaryHttpClient.java @@ -0,0 +1,259 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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.google.common.util.concurrent.ThreadFactoryBuilder; + +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_bungeecord.EaglerXBungee; +import net.md_5.bungee.netty.PipelineUtils; + +/** + * 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 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 responseCallback; + + protected NettyHttpChannelFutureListener(String method, URI requestURI, Consumer 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 EaglerXBungee/" + EaglerXBungee.getEagler().getDescription().getVersion()); + future.channel().writeAndFlush(request); + } else { + addressCache.invalidate(requestURI.getHost()); + responseCallback.accept(new Response(new IOException("Connection failed"))); + } + } + + } + + private static class NettyHttpChannelInitializer extends ChannelInitializer { + + protected final Consumer responseCallback; + protected final boolean ssl; + protected final String host; + protected final int port; + + protected NettyHttpChannelInitializer(Consumer 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 { + + protected final Consumer responseCallback; + protected int responseCode = -1; + protected ByteBuf buffer = null; + + protected NettyHttpResponseHandler(Consumer 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 addressCache = CacheBuilder.newBuilder().expireAfterWrite(15L, TimeUnit.MINUTES).build(); + private static EventLoopGroup eventLoop = null; + + public static void asyncRequest(String method, URI uri, Consumer 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()).channel(PipelineUtils.getChannel(null)).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 = PipelineUtils.newEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Skin Download Thread #%1$d").build()); + } + return eventLoop; + } + + public static void killEventLoop() { + if(eventLoop != null) { + EaglerXBungee.logger().info("Stopping skin cache HTTP client..."); + eventLoop.shutdownGracefully(); + try { + eventLoop.awaitTermination(30l, TimeUnit.SECONDS); + } catch (InterruptedException var13) { + ; + } + eventLoop = null; + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/CapePackets.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/CapePackets.java new file mode 100644 index 0000000..4828c7b --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/CapePackets.java @@ -0,0 +1,110 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins; + +import java.io.IOException; +import java.util.UUID; + +import net.md_5.bungee.UserConnection; + +/** + * 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, UserConnection 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, UserConnection 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; + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/CapeServiceOffline.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/CapeServiceOffline.java new file mode 100644 index 0000000..29fd6d2 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/CapeServiceOffline.java @@ -0,0 +1,64 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler; +import net.md_5.bungee.UserConnection; + +/** + * 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 String CHANNEL = "EAG|Capes-1.8"; + + private final Map capesCache = new HashMap(); + + public void registerEaglercraftPlayer(UUID playerUUID, byte[] capePacket) { + synchronized(capesCache) { + capesCache.put(playerUUID, capePacket); + } + } + + public void processGetOtherCape(UUID searchUUID, UserConnection sender) { + if(((EaglerInitialHandler)sender.getPendingConnection()).skinLookupRateLimiter.rateLimit(masterRateLimitPerPlayer)) { + byte[] maybeCape; + synchronized(capesCache) { + maybeCape = capesCache.get(searchUUID); + } + if(maybeCape != null) { + sender.sendData(CapeServiceOffline.CHANNEL, maybeCape); + }else { + sender.sendData(CapeServiceOffline.CHANNEL, CapePackets.makePresetResponse(searchUUID, 0)); + } + } + } + + public void unregisterPlayer(UUID playerUUID) { + synchronized(capesCache) { + capesCache.remove(playerUUID); + } + } + + public void shutdown() { + synchronized(capesCache) { + capesCache.clear(); + } + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/ICacheProvider.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/ICacheProvider.java new file mode 100644 index 0000000..5b13d1b --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/ICacheProvider.java @@ -0,0 +1,90 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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(); + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/ISkinService.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/ISkinService.java new file mode 100644 index 0000000..9edad7d --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/ISkinService.java @@ -0,0 +1,46 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins; + +import java.io.IOException; +import java.util.UUID; + +import net.md_5.bungee.UserConnection; + +/** + * 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 UserConnection sender); + + void processGetOtherSkin(UUID searchUUID, String skinURL, UserConnection sender); + + void registerEaglercraftPlayer(UUID clientUUID, byte[] generatedPacket, int modelId) throws IOException; + + 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(); + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/JDBCCacheProvider.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/JDBCCacheProvider.java new file mode 100644 index 0000000..f476175 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/JDBCCacheProvider.java @@ -0,0 +1,403 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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.logging.Level; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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); + } + EaglerXBungee.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); + EaglerXBungee.logger().warning("Skin object cache has passed " + maxObjects + " skins in size (" + + totalObjects + "), deleting " + deleteCount + " skins from the cache to free space"); + synchronized(deleteSomeOldestObjects) { + deleteSomeOldestObjects.setInt(1, deleteCount); + deleteSomeOldestObjects.executeUpdate(); + } + } + + if(totalProfiles > maxProfiles) { + int deleteCount = totalProfiles - maxProfiles + (maxProfiles >> 3); + EaglerXBungee.logger().warning("Skin profile cache has passed " + maxProfiles + " profiles in size (" + + totalProfiles + "), deleting " + deleteCount + " profiles from the cache to free space"); + 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(); + EaglerXBungee.logger().info("Successfully disconnected from database '" + uri + "'"); + } catch (SQLException e) { + EaglerXBungee.logger().log(Level.WARNING, "Exception disconnecting from database '" + uri + "'!", e); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SimpleRateLimiter.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SimpleRateLimiter.java new file mode 100644 index 0000000..67b0eb5 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SimpleRateLimiter.java @@ -0,0 +1,47 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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; + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinPackets.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinPackets.java new file mode 100644 index 0000000..fd8349e --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinPackets.java @@ -0,0 +1,259 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.md_5.bungee.UserConnection; + +/** + * 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, UserConnection 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, UserConnection 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, UserConnection 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(EaglerXBungee.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) throws IOException { + 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; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinRescaler.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinRescaler.java new file mode 100644 index 0000000..29b2b54 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinRescaler.java @@ -0,0 +1,76 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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); + } + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinService.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinService.java new file mode 100644 index 0000000..71ffc44 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinService.java @@ -0,0 +1,1005 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins; + +import java.io.IOException; +import java.net.URI; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import org.apache.commons.codec.binary.Base64; + +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import gnu.trove.map.TObjectIntMap; +import gnu.trove.map.hash.TObjectIntHashMap; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerBungeeConfig; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.AsyncSkinProvider.CacheFetchedProfile; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins.AsyncSkinProvider.CancelException; +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.UserConnection; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.connection.LoginResult; +import net.md_5.bungee.protocol.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 SkinService implements ISkinService { + + public static final int masterRateLimitPerPlayer = 250; + + public static final String CHANNEL = "EAG|Skins-1.8"; + + private final Map onlinePlayersCache = new HashMap(); + private final Multimap onlinePlayersFromTexturesMap = MultimapBuilder.hashKeys().hashSetValues().build(); + private final Map onlinePlayersToTexturesMap = new HashMap(); + private final Map foreignSkinCache = new HashMap(); + + private final Map pendingTextures = new HashMap(); + private final Map pendingUUIDs = new HashMap(); + private final Map pendingNameLookups = new HashMap(); + + private final TObjectIntMap antagonists = new TObjectIntHashMap(); + private long antagonistCooldown = System.currentTimeMillis(); + + private final Consumer> antagonistLogger = new Consumer>() { + + @Override + public void accept(Set t) { + if(t.size() == 1) { + int limit = EaglerXBungee.getEagler().getConfig().getAntagonistsRateLimit() << 1; + UUID offender = t.iterator().next(); + synchronized(antagonists) { + int v = antagonists.get(offender); + if(v == antagonists.getNoEntryValue()) { + antagonists.put(offender, 1); + }else { + if(v <= limit) { + antagonists.put(offender, v + 1); + } + } + } + } + } + + }; + + private ICacheProvider cacheProvider = null; + + protected static class CachedForeignSkin { + + protected final UUID uuid; + protected final byte[] data; + protected final int modelKnown; + protected long lastHit; + + protected CachedForeignSkin(UUID uuid, byte[] data, int modelKnown) { + this.uuid = uuid; + this.data = data; + this.modelKnown = modelKnown; + this.lastHit = System.currentTimeMillis(); + } + + } + + protected static class CachedPlayerSkin { + + protected final byte[] data; + protected final UUID textureUUID; + protected final int modelId; + + protected CachedPlayerSkin(byte[] data, UUID textureUUID, int modelId) { + this.data = data; + this.textureUUID = textureUUID; + this.modelId = modelId; + } + + } + + protected class PendingTextureDownload implements Consumer { + + protected final UUID textureUUID; + protected final String textureURL; + + protected final Set antagonists; + protected final List> callbacks; + protected final Consumer> antagonistsCallback; + + protected final long initializedTime; + protected boolean finalized; + + protected PendingTextureDownload(UUID textureUUID, String textureURL, UUID caller, Consumer callback, + Consumer> antagonistsCallback) { + this.textureUUID = textureUUID; + this.textureURL = textureURL; + this.antagonists = new LinkedHashSet(); + this.antagonists.add(caller); + this.callbacks = new LinkedList(); + this.callbacks.add(callback); + this.antagonistsCallback = antagonistsCallback; + this.initializedTime = System.currentTimeMillis(); + this.finalized = false; + } + + @Override + public void accept(byte[] t) { + for(int i = 0, l = callbacks.size(); i < l; ++i) { + try { + callbacks.get(i).accept(t); + }catch(Throwable t2) { + } + } + if(t != null) { + synchronized(pendingTextures) { + finalized = true; + pendingTextures.remove(textureUUID); + } + } + } + + } + + protected class PendingProfileUUIDLookup implements Consumer { + + protected final UUID profileUUID; + + protected final Set antagonists; + protected final List> callbacks; + protected final Consumer> antagonistsCallback; + + protected final long initializedTime; + protected boolean finalized; + + protected PendingProfileUUIDLookup(UUID profileUUID, UUID caller, Consumer callback, + Consumer> antagonistsCallback) { + this.profileUUID = profileUUID; + this.antagonists = new LinkedHashSet(); + this.antagonists.add(caller); + this.callbacks = new LinkedList(); + this.callbacks.add(callback); + this.antagonistsCallback = antagonistsCallback; + this.initializedTime = System.currentTimeMillis(); + this.finalized = false; + } + + @Override + public void accept(CacheFetchedProfile t) { + for(int i = 0, l = callbacks.size(); i < l; ++i) { + try { + callbacks.get(i).accept(t); + }catch(Throwable t2) { + } + } + if(t != null) { + synchronized(pendingUUIDs) { + finalized = true; + pendingUUIDs.remove(profileUUID); + } + } + } + + } + + protected class PendingProfileNameLookup implements Consumer { + + protected final String profileName; + + protected final Set antagonists; + protected final List> callbacks; + protected final Consumer> antagonistsCallback; + + protected final long initializedTime; + protected boolean finalized; + + protected PendingProfileNameLookup(String profileName, UUID caller, Consumer callback, + Consumer> antagonistsCallback) { + this.profileName = profileName; + this.antagonists = new LinkedHashSet(); + this.antagonists.add(caller); + this.callbacks = new LinkedList(); + this.callbacks.add(callback); + this.antagonistsCallback = antagonistsCallback; + this.initializedTime = System.currentTimeMillis(); + this.finalized = false; + } + + @Override + public void accept(CacheFetchedProfile t) { + for(int i = 0, l = callbacks.size(); i < l; ++i) { + try { + callbacks.get(i).accept(t); + }catch(Throwable t2) { + } + } + if(t != null) { + synchronized(pendingNameLookups) { + finalized = true; + pendingNameLookups.remove(profileName); + } + } + } + + } + + public void init(String uri, String driverClass, String driverPath, int keepObjectsDays, int keepProfilesDays, + int maxObjects, int maxProfiles) { + antagonistCooldown = System.currentTimeMillis(); + if(cacheProvider == null) { + cacheProvider = JDBCCacheProvider.initialize(uri, driverClass, driverPath, keepObjectsDays, + keepProfilesDays, maxObjects, maxProfiles); + } + resetMaps(); + } + + public void processGetOtherSkin(final UUID searchUUID, final UserConnection sender) { + EaglerInitialHandler eaglerHandler = (EaglerInitialHandler)sender.getPendingConnection(); + if(!eaglerHandler.skinLookupRateLimiter.rateLimit(masterRateLimitPerPlayer)) { + return; + } + + CachedPlayerSkin maybeCachedPacket; + synchronized(onlinePlayersCache) { + maybeCachedPacket = onlinePlayersCache.get(searchUUID); + } + + if(maybeCachedPacket != null) { + sender.sendData(SkinService.CHANNEL, maybeCachedPacket.data); + }else { + ProxiedPlayer player = BungeeCord.getInstance().getPlayer(searchUUID); + UUID playerTexture; + synchronized(onlinePlayersToTexturesMap) { + playerTexture = onlinePlayersToTexturesMap.get(searchUUID); + } + if(playerTexture != null) { + Collection possiblePlayers; + synchronized(onlinePlayersFromTexturesMap) { + possiblePlayers = onlinePlayersFromTexturesMap.get(playerTexture); + } + boolean playersExist = possiblePlayers.size() > 0; + if(playersExist) { + for(UUID uuid : possiblePlayers) { + synchronized(onlinePlayersCache) { + maybeCachedPacket = onlinePlayersCache.get(uuid); + } + if(maybeCachedPacket != null) { + byte[] rewritten = SkinPackets.rewriteUUID(searchUUID, maybeCachedPacket.data); + if(player != null) { + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(searchUUID, new CachedPlayerSkin(rewritten, + maybeCachedPacket.textureUUID, maybeCachedPacket.modelId)); + } + } + sender.sendData(SkinService.CHANNEL, rewritten); + return; + } + } + } + CachedForeignSkin foreignSkin; + synchronized(foreignSkinCache) { + foreignSkin = foreignSkinCache.get(playerTexture); + } + if(foreignSkin != null && foreignSkin.modelKnown != -1) { + if(player != null) { + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(searchUUID, + new CachedPlayerSkin(SkinPackets.rewriteUUID(searchUUID, foreignSkin.data), + playerTexture, foreignSkin.modelKnown)); + } + synchronized(foreignSkinCache) { + foreignSkinCache.remove(playerTexture); + } + }else { + foreignSkin.lastHit = System.currentTimeMillis(); + } + sender.sendData(SkinService.CHANNEL, foreignSkin.data); + return; + } + } + if(player != null && (player instanceof UserConnection)) { + if(player instanceof UserConnection) { + LoginResult loginProfile = ((UserConnection)player).getPendingConnection().getLoginProfile(); + if(loginProfile != null) { + Property[] props = loginProfile.getProperties(); + if(props.length > 0) { + for(int i = 0; i < props.length; ++i) { + Property pp = props[i]; + if(pp.getName().equals("textures")) { + try { + String jsonStr = SkinPackets.bytesToAscii(Base64.decodeBase64(pp.getValue())); + JsonObject json = (new JsonParser()).parse(jsonStr).getAsJsonObject(); + JsonObject skinObj = json.getAsJsonObject("SKIN"); + if(skinObj != null) { + JsonElement url = json.get("url"); + if(url != null) { + String urlStr = url.getAsString(); + urlStr = SkinService.sanitizeTextureURL(urlStr); + if(urlStr == null) { + break; + } + int model = 0; + JsonElement el = skinObj.get("metadata"); + if(el != null && el.isJsonObject()) { + el = el.getAsJsonObject().get("model"); + if(el != null) { + model = SkinPackets.getModelId(el.getAsString()); + } + } + UUID skinUUID = SkinPackets.createEaglerURLSkinUUID(urlStr); + + CachedForeignSkin foreignSkin; + synchronized(foreignSkinCache) { + foreignSkin = foreignSkinCache.remove(skinUUID); + } + if(foreignSkin != null) { + registerTextureToPlayerAssociation(skinUUID, searchUUID); + byte[] rewrite = SkinPackets.rewriteUUIDModel(searchUUID, foreignSkin.data, model); + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(searchUUID, new CachedPlayerSkin( + rewrite, skinUUID, model)); + } + sender.sendData(SkinService.CHANNEL, rewrite); + return; + } + + // download player skin, put in onlinePlayersCache, no limit + + if(!isLimitedAsAntagonist(player.getUniqueId())) { + processResolveURLTextureForOnline(sender, searchUUID, skinUUID, urlStr, model); + } + + return; + } + } + }catch(Throwable t) { + } + } + } + } + } + } + if(!isLimitedAsAntagonist(player.getUniqueId())) { + if(player.getPendingConnection().isOnlineMode()) { + processResolveProfileTextureByUUIDForOnline(sender, searchUUID); + }else { + processResolveProfileTextureByNameForOnline(sender, player.getPendingConnection().getName(), searchUUID); + } + } + }else { + CachedForeignSkin foreignSkin; + synchronized(foreignSkinCache) { + foreignSkin = foreignSkinCache.get(searchUUID); + } + if(foreignSkin != null) { + foreignSkin.lastHit = System.currentTimeMillis(); + sender.sendData(SkinService.CHANNEL, foreignSkin.data); + }else { + if (eaglerHandler.skinUUIDLookupRateLimiter + .rateLimit(EaglerXBungee.getEagler().getConfig().getUuidRateLimitPlayer()) + && !isLimitedAsAntagonist(sender.getUniqueId())) { + processResolveProfileTextureByUUIDForeign(sender, searchUUID); + } + } + } + } + } + + public void processGetOtherSkin(UUID searchUUID, String skinURL, UserConnection sender) { + EaglerBungeeConfig config = EaglerXBungee.getEagler().getConfig(); + EaglerInitialHandler eaglerHandler = (EaglerInitialHandler)sender.getPendingConnection(); + if(!eaglerHandler.skinLookupRateLimiter.rateLimit(masterRateLimitPerPlayer)) { + return; + } + CachedForeignSkin foreignSkin; + synchronized(foreignSkinCache) { + foreignSkin = foreignSkinCache.get(searchUUID); + } + if(foreignSkin != null) { + foreignSkin.lastHit = System.currentTimeMillis(); + sender.sendData(SkinService.CHANNEL, foreignSkin.data); + }else { + Collection possiblePlayers; + synchronized(onlinePlayersFromTexturesMap) { + possiblePlayers = onlinePlayersFromTexturesMap.get(searchUUID); + } + boolean playersExist = possiblePlayers.size() > 0; + if(playersExist) { + for(UUID uuid : possiblePlayers) { + CachedPlayerSkin maybeCachedPacket; + synchronized(onlinePlayersCache) { + maybeCachedPacket = onlinePlayersCache.get(uuid); + } + if(maybeCachedPacket != null) { + byte[] rewritten = SkinPackets.rewriteUUID(searchUUID, maybeCachedPacket.data); + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(searchUUID, maybeCachedPacket); + } + sender.sendData(SkinService.CHANNEL, rewritten); + return; + } + } + } + if(eaglerHandler.skinTextureDownloadRateLimiter.rateLimit(config.getSkinRateLimitPlayer()) && !isLimitedAsAntagonist(sender.getUniqueId())) { + processResolveURLTextureForForeign(sender, searchUUID, searchUUID, skinURL, -1); + } + } + } + + private void processResolveURLTextureForOnline(final UserConnection initiator, final UUID onlineCacheUUID, + final UUID skinUUID, final String urlStr, final int modelId) { + synchronized(pendingTextures) { + PendingTextureDownload alreadyPending = pendingTextures.get(skinUUID); + if(alreadyPending != null) { + if(alreadyPending.antagonists.add(initiator.getUniqueId())) { + alreadyPending.callbacks.add(new Consumer() { + + @Override + public void accept(byte[] t) { + CachedPlayerSkin skin; + synchronized(onlinePlayersCache) { + skin = onlinePlayersCache.get(onlineCacheUUID); + } + if(skin != null) { + initiator.sendData(SkinService.CHANNEL, skin.data); + } + } + + }); + } + }else { + PendingTextureDownload newTask = new PendingTextureDownload( + skinUUID, urlStr, initiator.getUniqueId(), new Consumer() { + + @Override + public void accept(byte[] t) { + CachedPlayerSkin skin; + if(t != null) { + registerTextureToPlayerAssociation(skinUUID, onlineCacheUUID); + skin = new CachedPlayerSkin(SkinPackets.makeCustomResponse(onlineCacheUUID, modelId, t), skinUUID, modelId); + }else { + skin = new CachedPlayerSkin(SkinPackets.makePresetResponse(onlineCacheUUID), null, -1); + } + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(onlineCacheUUID, skin); + } + initiator.sendData(SkinService.CHANNEL, skin.data); + } + + }, antagonistLogger); + try { + AsyncSkinProvider.downloadSkin(skinUUID, urlStr, cacheProvider, newTask); + }catch(CancelException ex) { + return; + } + pendingTextures.put(skinUUID, newTask); + } + } + } + + private void processResolveURLTextureForForeign(final UserConnection initiator, final UUID foreignCacheUUID, + final UUID skinUUID, final String urlStr, final int modelId) { + synchronized(pendingTextures) { + PendingTextureDownload alreadyPending = pendingTextures.get(skinUUID); + if(alreadyPending != null) { + if(alreadyPending.antagonists.add(initiator.getUniqueId())) { + alreadyPending.callbacks.add(new Consumer() { + + @Override + public void accept(byte[] t) { + CachedForeignSkin skin; + synchronized(foreignSkinCache) { + skin = foreignSkinCache.get(foreignCacheUUID); + } + if(skin != null) { + initiator.sendData(SkinService.CHANNEL, skin.data); + } + } + + }); + } + }else { + PendingTextureDownload newTask = new PendingTextureDownload(skinUUID, urlStr, initiator.getUniqueId(), new Consumer() { + + @Override + public void accept(byte[] t) { + CachedForeignSkin skin; + if(t != null) { + skin = new CachedForeignSkin(foreignCacheUUID, SkinPackets.makeCustomResponse(foreignCacheUUID, modelId, t), modelId); + }else { + skin = new CachedForeignSkin(foreignCacheUUID, SkinPackets.makePresetResponse(foreignCacheUUID), -1); + } + synchronized(foreignSkinCache) { + foreignSkinCache.put(foreignCacheUUID, skin); + } + initiator.sendData(SkinService.CHANNEL, skin.data); + } + + }, antagonistLogger); + try { + AsyncSkinProvider.downloadSkin(skinUUID, urlStr, cacheProvider, newTask); + }catch(CancelException ex) { + return; + } + pendingTextures.put(skinUUID, newTask); + } + } + } + + private void processResolveProfileTextureByUUIDForOnline(final UserConnection initiator, final UUID playerUUID) { + synchronized(pendingUUIDs) { + PendingProfileUUIDLookup alreadyPending = pendingUUIDs.get(playerUUID); + if(alreadyPending != null) { + if(alreadyPending.antagonists.add(initiator.getUniqueId())) { + alreadyPending.callbacks.add(new Consumer() { + + @Override + public void accept(CacheFetchedProfile t) { + if(t == null || t.texture == null) { + CachedPlayerSkin skin; + synchronized(onlinePlayersCache) { + skin = onlinePlayersCache.get(playerUUID); + } + if(skin != null) { + initiator.sendData(SkinService.CHANNEL, skin.data); + } + }else { + processResolveURLTextureForOnline(initiator, playerUUID, t.textureUUID, t.texture, + SkinPackets.getModelId(t.model)); + } + } + + }); + } + }else { + PendingProfileUUIDLookup newTask = new PendingProfileUUIDLookup( + playerUUID, initiator.getUniqueId(), new Consumer() { + + @Override + public void accept(CacheFetchedProfile t) { + if(t == null || t.texture == null) { + CachedPlayerSkin skin; + if(t == null) { + skin = new CachedPlayerSkin(SkinPackets.makePresetResponse(playerUUID), null, -1); + }else { + skin = new CachedPlayerSkin(SkinPackets.makePresetResponse(playerUUID, + SkinPackets.getModelId(t.model) == 1 ? 1 : 0), null, -1); + } + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(playerUUID, skin); + } + initiator.sendData(SkinService.CHANNEL, skin.data); + }else { + processResolveURLTextureForOnline(initiator, playerUUID, t.textureUUID, t.texture, + SkinPackets.getModelId(t.model)); + } + } + + }, antagonistLogger); + try { + AsyncSkinProvider.lookupProfileByUUID(playerUUID, cacheProvider, newTask); + }catch(CancelException ex) { + return; + } + pendingUUIDs.put(playerUUID, newTask); + } + } + } + + private void processResolveProfileTextureByNameForOnline(final UserConnection initiator, final String playerName, final UUID mapUUID) { + synchronized(pendingNameLookups) { + PendingProfileNameLookup alreadyPending = pendingNameLookups.get(playerName); + if(alreadyPending != null) { + if(alreadyPending.antagonists.add(initiator.getUniqueId())) { + alreadyPending.callbacks.add(new Consumer() { + + @Override + public void accept(CacheFetchedProfile t) { + if(t == null || t.texture == null) { + CachedPlayerSkin skin; + synchronized(onlinePlayersCache) { + skin = onlinePlayersCache.get(t.uuid); + } + if(skin != null) { + initiator.sendData(SkinService.CHANNEL, skin.data); + } + }else { + processResolveURLTextureForOnline(initiator, mapUUID, t.textureUUID, t.texture, + SkinPackets.getModelId(t.model)); + } + } + + }); + } + }else { + PendingProfileNameLookup newTask = new PendingProfileNameLookup( + playerName, initiator.getUniqueId(), new Consumer() { + + @Override + public void accept(CacheFetchedProfile t) { + if(t == null || t.texture == null) { + CachedPlayerSkin skin; + if(t == null) { + skin = new CachedPlayerSkin(SkinPackets.makePresetResponse(mapUUID), null, -1); + }else { + skin = new CachedPlayerSkin(SkinPackets.makePresetResponse(mapUUID, + SkinPackets.getModelId(t.model) == 1 ? 1 : 0), null, -1); + } + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(mapUUID, skin); + } + initiator.sendData(SkinService.CHANNEL, skin.data); + }else { + processResolveURLTextureForOnline(initiator, mapUUID, t.textureUUID, t.texture, + SkinPackets.getModelId(t.model)); + } + } + + }, antagonistLogger); + try { + AsyncSkinProvider.lookupProfileByUsername(playerName, cacheProvider, newTask); + }catch(CancelException ex) { + return; + } + pendingNameLookups.put(playerName, newTask); + } + } + } + + private void processResolveProfileTextureByUUIDForeign(final UserConnection initiator, final UUID playerUUID) { + synchronized(pendingUUIDs) { + PendingProfileUUIDLookup alreadyPending = pendingUUIDs.get(playerUUID); + if(alreadyPending != null) { + if(alreadyPending.antagonists.add(initiator.getUniqueId())) { + alreadyPending.callbacks.add(new Consumer() { + + @Override + public void accept(CacheFetchedProfile t) { + if(t == null || t.texture == null) { + CachedForeignSkin skin; + synchronized(foreignSkinCache) { + skin = foreignSkinCache.get(playerUUID); + } + if(skin != null) { + initiator.sendData(SkinService.CHANNEL, skin.data); + } + }else { + processResolveURLTextureForForeign(initiator, playerUUID, t.textureUUID, t.texture, + SkinPackets.getModelId(t.model)); + } + } + + }); + } + }else { + PendingProfileUUIDLookup newTask = new PendingProfileUUIDLookup( + playerUUID, initiator.getUniqueId(), new Consumer() { + + @Override + public void accept(CacheFetchedProfile t) { + if(t == null || t.texture == null) { + CachedForeignSkin skin; + if(t == null) { + skin = new CachedForeignSkin(playerUUID, SkinPackets.makePresetResponse(playerUUID), -1); + }else { + skin = new CachedForeignSkin(playerUUID, SkinPackets.makePresetResponse( + playerUUID, SkinPackets.getModelId(t.model) == 1 ? 1 : 0), -1); + } + synchronized(foreignSkinCache) { + foreignSkinCache.put(playerUUID, skin); + } + initiator.sendData(SkinService.CHANNEL, skin.data); + }else { + processResolveURLTextureForForeign(initiator, playerUUID, t.textureUUID, t.texture, + SkinPackets.getModelId(t.model)); + } + } + + }, antagonistLogger); + try { + AsyncSkinProvider.lookupProfileByUUID(playerUUID, cacheProvider, newTask); + }catch(CancelException ex) { + return; + } + pendingUUIDs.put(playerUUID, newTask); + } + } + } + + public void registerEaglercraftPlayer(UUID clientUUID, byte[] generatedPacket, int modelId) throws IOException { + synchronized(foreignSkinCache) { + foreignSkinCache.remove(clientUUID); + } + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(clientUUID, new CachedPlayerSkin(generatedPacket, null, modelId)); + } + } + + public void unregisterPlayer(UUID clientUUID) { + CachedPlayerSkin data; + synchronized(onlinePlayersCache) { + data = onlinePlayersCache.remove(clientUUID); + } + if(data != null) { + synchronized(foreignSkinCache) { + foreignSkinCache.put(clientUUID, new CachedForeignSkin(clientUUID, data.data, data.modelId)); + } + if(data.textureUUID != null) { + synchronized(foreignSkinCache) { + foreignSkinCache.put(data.textureUUID, new CachedForeignSkin(data.textureUUID, data.data, data.modelId)); + } + } + deletePlayerTextureAssociation(clientUUID, data.textureUUID); + }else { + deletePlayerTextureAssociation(clientUUID, null); + } + } + + private void deletePlayerTextureAssociation(UUID clientUUID, UUID textureUUID) { + if(textureUUID != null) { + synchronized(onlinePlayersToTexturesMap) { + onlinePlayersToTexturesMap.remove(clientUUID); + } + synchronized(onlinePlayersFromTexturesMap) { + onlinePlayersFromTexturesMap.remove(textureUUID, clientUUID); + } + }else { + UUID removedUUID; + synchronized(onlinePlayersToTexturesMap) { + removedUUID = onlinePlayersToTexturesMap.remove(clientUUID); + } + if(removedUUID != null) { + synchronized(onlinePlayersFromTexturesMap) { + onlinePlayersFromTexturesMap.remove(removedUUID, clientUUID); + } + } + } + } + + public void registerTextureToPlayerAssociation(UUID textureUUID, UUID playerUUID) { + synchronized(onlinePlayersFromTexturesMap) { + onlinePlayersFromTexturesMap.put(textureUUID, playerUUID); + } + synchronized(onlinePlayersToTexturesMap) { + onlinePlayersToTexturesMap.put(playerUUID, textureUUID); + } + CachedForeignSkin foreign; + synchronized(foreignSkinCache) { + foreign = foreignSkinCache.remove(textureUUID); + } + if(foreign != null) { + synchronized(onlinePlayersCache) { + onlinePlayersCache.put(playerUUID, new CachedPlayerSkin(foreign.data, textureUUID, foreign.modelKnown)); + } + } + } + + public void flush() { + long millis = System.currentTimeMillis(); + + synchronized(foreignSkinCache) { + Iterator itr = foreignSkinCache.values().iterator(); + while(itr.hasNext()) { + if(millis - itr.next().lastHit > 900000l) { // 15 minutes + itr.remove(); + } + } + } + + synchronized(pendingTextures) { + Iterator itr = pendingTextures.values().iterator(); + while(itr.hasNext()) { + PendingTextureDownload etr = itr.next(); + if(millis - etr.initializedTime > (etr.finalized ? 5000l : 10000l)) { + itr.remove(); + try { + etr.antagonistsCallback.accept(etr.antagonists); + }catch(Throwable t) { + } + } + } + } + + synchronized(pendingUUIDs) { + Iterator itr = pendingUUIDs.values().iterator(); + while(itr.hasNext()) { + PendingProfileUUIDLookup etr = itr.next(); + if(millis - etr.initializedTime > (etr.finalized ? 5000l : 10000l)) { + itr.remove(); + try { + etr.antagonistsCallback.accept(etr.antagonists); + }catch(Throwable t) { + } + } + } + } + + synchronized(pendingNameLookups) { + Iterator itr = pendingNameLookups.values().iterator(); + while(itr.hasNext()) { + PendingProfileNameLookup etr = itr.next(); + if(millis - etr.initializedTime > (etr.finalized ? 5000l : 10000l)) { + itr.remove(); + try { + etr.antagonistsCallback.accept(etr.antagonists); + }catch(Throwable t) { + } + } + } + } + + int cooldownPeriod = 60000 / EaglerXBungee.getEagler().getConfig().getAntagonistsRateLimit(); + int elapsedCooldown = (int)(millis - antagonistCooldown); + elapsedCooldown /= cooldownPeriod; + if(elapsedCooldown > 0) { + antagonistCooldown += elapsedCooldown * cooldownPeriod; + synchronized(antagonists) { + Iterator itr = antagonists.keySet().iterator(); + while(itr.hasNext()) { + UUID key = itr.next(); + int i = antagonists.get(key) - elapsedCooldown; + if(i <= 0) { + itr.remove(); + }else { + antagonists.put(key, i); + } + } + } + } + + cacheProvider.flush(); + } + + public void shutdown() { + resetMaps(); + if(cacheProvider != null) { + cacheProvider.destroy(); + } + cacheProvider = null; + } + + private boolean isLimitedAsAntagonist(UUID uuid) { + int limit = EaglerXBungee.getEagler().getConfig().getAntagonistsRateLimit(); + limit += limit >> 1; + synchronized(antagonists) { + int i = antagonists.get(uuid); + return i != antagonists.getNoEntryValue() && i > limit; + } + } + + private void resetMaps() { + synchronized(onlinePlayersCache) { + onlinePlayersCache.clear(); + } + synchronized(onlinePlayersFromTexturesMap) { + onlinePlayersFromTexturesMap.clear(); + } + synchronized(onlinePlayersToTexturesMap) { + onlinePlayersToTexturesMap.clear(); + } + synchronized(foreignSkinCache) { + foreignSkinCache.clear(); + } + synchronized(pendingTextures) { + pendingTextures.clear(); + } + synchronized(pendingUUIDs) { + pendingUUIDs.clear(); + } + synchronized(pendingNameLookups) { + pendingNameLookups.clear(); + } + synchronized(antagonists) { + antagonists.clear(); + } + } + + public static String sanitizeTextureURL(String url) { + try { + URI uri = URI.create(url); + StringBuilder builder = new StringBuilder(); + String scheme = uri.getScheme(); + if(scheme == null) { + return null; + } + String host = uri.getHost(); + if(host == null) { + return null; + } + scheme = scheme.toLowerCase(); + builder.append(scheme).append("://"); + builder.append(host); + int port = uri.getPort(); + if(port != -1) { + switch(scheme) { + case "http": + if(port == 80) { + port = -1; + } + break; + case "https": + if(port == 443) { + port = -1; + } + break; + default: + return null; + } + if(port != -1) { + builder.append(":").append(port); + } + } + String path = uri.getRawPath(); + if(path != null) { + if(path.contains("//")) { + path = String.join("/", path.split("[\\/]+")); + } + int len = path.length(); + if(len > 1 && path.charAt(len - 1) == '/') { + path = path.substring(0, len - 1); + } + builder.append(path); + } + return builder.toString(); + }catch(Throwable t) { + return null; + } + } + + 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); + } + + public static boolean isAlex(UUID skinUUID) { + return (skinUUID.hashCode() & 1) != 0; + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinServiceOffline.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinServiceOffline.java new file mode 100644 index 0000000..0be7014 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/skins/SkinServiceOffline.java @@ -0,0 +1,120 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.skins; + +import java.io.IOException; +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 net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler; +import net.md_5.bungee.UserConnection; + +/** + * 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 skinCache = new HashMap(); + + private final Multimap 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, UserConnection sender) { + if(((EaglerInitialHandler)sender.getPendingConnection()).skinLookupRateLimiter.rateLimit(masterRateLimitPerPlayer)) { + CachedSkin cached; + synchronized(skinCache) { + cached = skinCache.get(searchUUID); + } + if(cached != null) { + sender.sendData(SkinService.CHANNEL, cached.packet); + }else { + sender.sendData(SkinService.CHANNEL, SkinPackets.makePresetResponse(searchUUID)); + } + } + } + + public void processGetOtherSkin(UUID searchUUID, String skinURL, UserConnection sender) { + Collection uuids; + synchronized(onlinePlayersFromTexturesMap) { + uuids = onlinePlayersFromTexturesMap.get(searchUUID); + } + if(uuids.size() > 0) { + CachedSkin cached; + synchronized(skinCache) { + Iterator uuidItr = uuids.iterator(); + while(uuidItr.hasNext()) { + cached = skinCache.get(uuidItr.next()); + if(cached != null) { + sender.sendData(SkinService.CHANNEL, SkinPackets.rewriteUUID(searchUUID, cached.packet)); + } + } + } + } + sender.sendData(SkinService.CHANNEL, SkinPackets.makePresetResponse(searchUUID)); + } + + public void registerEaglercraftPlayer(UUID clientUUID, byte[] generatedPacket, int modelId) throws IOException { + 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(); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/sqlite/EaglerDrivers.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/sqlite/EaglerDrivers.java new file mode 100644 index 0000000..07ccdc8 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/sqlite/EaglerDrivers.java @@ -0,0 +1,121 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.sqlite; + +import java.io.File; +import java.io.IOException; +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 org.codehaus.plexus.util.FileUtils; + +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee; + +/** + * 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(EaglerXBungee.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"); + EaglerXBungee.logger().info("Downloading from maven: " + u.toString()); + FileUtils.copyURLToFile(u, driver); + } catch (Throwable ex) { + EaglerXBungee.logger().severe("Could not download sqlite-jdbc.jar from repo1.maven.org!"); + EaglerXBungee.logger().severe("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) { + EaglerXBungee.logger().severe("Invalid JDBC driver path: " + address); + throw new ExceptionInInitializerError(ex); + } + classLoader = new URLClassLoader(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) { + } + EaglerXBungee.logger().severe("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) { + } + EaglerXBungee.logger().severe("Could not initialize JDBC driver class: " + driverClass); + throw new ExceptionInInitializerError(ex); + } + + return sqlDriver; + } + + private static final Map driversJARs = new HashMap(); + private static final Map 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); + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/ExpiringSet.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/ExpiringSet.java new file mode 100644 index 0000000..547e094 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/ExpiringSet.java @@ -0,0 +1,84 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 extends HashSet { + private final long expiration; + private final ExpiringEvent event; + + private final Map timestamps = new HashMap<>(); + + public ExpiringSet(long expiration) { + this.expiration = expiration; + this.event = null; + } + + public ExpiringSet(long expiration, ExpiringEvent event) { + this.expiration = expiration; + this.event = event; + } + + public interface ExpiringEvent { + void onExpiration(T item); + } + + public void checkForExpirations() { + Iterator 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); + } +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceServerImpl.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceServerImpl.java new file mode 100644 index 0000000..392a09a --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceServerImpl.java @@ -0,0 +1,243 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.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 net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.server.EaglerInitialHandler; +import net.md_5.bungee.UserConnection; +import net.md_5.bungee.api.config.ServerInfo; + +/** + * 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 voicePlayers = new HashMap<>(); + private final Map> voiceRequests = new HashMap<>(); + private final Set 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(UserConnection player) { + player.sendData(VoiceService.CHANNEL, iceServersPacket); + } + + public void handlePlayerLoggedOut(UserConnection player) { + removeUser(player.getUniqueId()); + } + + void handleVoiceSignalPacketTypeRequest(UUID player, UserConnection sender) { + synchronized (voicePlayers) { + UUID senderUUID = sender.getUniqueId(); + if (senderUUID.equals(player)) + return; // prevent duplicates + if (!voicePlayers.containsKey(senderUUID)) + return; + UserConnection targetPlayerCon = voicePlayers.get(player); + if (targetPlayerCon == null) + return; + VoicePair newPair = new VoicePair(player, senderUUID); + if (voicePairs.contains(newPair)) + return; // already paired + ExpiringSet 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 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.sendData(VoiceService.CHANNEL, + VoiceSignalPackets.makeVoiceSignalPacketConnect(senderUUID, false)); + sender.sendData(VoiceService.CHANNEL, VoiceSignalPackets.makeVoiceSignalPacketConnect(player, true)); + } + } + } + + void handleVoiceSignalPacketTypeConnect(UserConnection sender) { + if(!((EaglerInitialHandler)sender.getPendingConnection()).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 (UserConnection userCon : voicePlayers.values()) { + userCon.sendData(VoiceService.CHANNEL, packetToBroadcast); + } + } + } + + void handleVoiceSignalPacketTypeICE(UUID player, String str, UserConnection sender) { + UserConnection pass; + VoicePair pair = new VoicePair(player, sender.getUniqueId()); + synchronized (voicePlayers) { + pass = voicePairs.contains(pair) ? voicePlayers.get(player) : null; + } + if (pass != null) { + pass.sendData(VoiceService.CHANNEL, VoiceSignalPackets.makeVoiceSignalPacketICE(sender.getUniqueId(), str)); + } + } + + void handleVoiceSignalPacketTypeDesc(UUID player, String str, UserConnection sender) { + UserConnection pass; + VoicePair pair = new VoicePair(player, sender.getUniqueId()); + synchronized (voicePlayers) { + pass = voicePairs.contains(pair) ? voicePlayers.get(player) : null; + } + if (pass != null) { + pass.sendData(VoiceService.CHANNEL, + VoiceSignalPackets.makeVoiceSignalPacketDesc(sender.getUniqueId(), str)); + } + } + + void handleVoiceSignalPacketTypeDisconnect(UUID player, UserConnection sender) { + if (player != null) { + synchronized (voicePlayers) { + if (!voicePlayers.containsKey(player)) { + return; + } + byte[] userDisconnectPacket = null; + Iterator 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(); + UserConnection conn = voicePlayers.get(target); + if (conn != null) { + if (userDisconnectPacket == null) { + userDisconnectPacket = VoiceSignalPackets.makeVoiceSignalPacketDisconnect(player); + } + conn.sendData(VoiceService.CHANNEL, userDisconnectPacket); + } + sender.sendData(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 (UserConnection userCon : voicePlayers.values()) { + if (!user.equals(userCon.getUniqueId())) { + userCon.sendData(VoiceService.CHANNEL, voicePlayersPkt); + } + } + } + byte[] userDisconnectPacket = null; + Iterator 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) { + UserConnection conn = voicePlayers.get(target); + if (conn != null) { + if (userDisconnectPacket == null) { + userDisconnectPacket = VoiceSignalPackets.makeVoiceSignalPacketDisconnect(user); + } + conn.sendData(VoiceService.CHANNEL, userDisconnectPacket); + } + } + } + } + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceService.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceService.java new file mode 100644 index 0000000..43b7696 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceService.java @@ -0,0 +1,118 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.voice; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import gnu.trove.map.TMap; +import net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.config.EaglerBungeeConfig; +import net.md_5.bungee.BungeeCord; +import net.md_5.bungee.UserConnection; +import net.md_5.bungee.api.config.ServerInfo; + +/** + * 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 String CHANNEL = "EAG|Voice-1.8"; + + private final Map serverMap = new HashMap(); + private final byte[] disableVoicePacket; + + public VoiceService(EaglerBungeeConfig conf) { + this.disableVoicePacket = VoiceSignalPackets.makeVoiceSignalPacketAllowed(false, null); + String[] iceServers = conf.getICEServers().toArray(new String[conf.getICEServers().size()]); + byte[] iceServersPacket = VoiceSignalPackets.makeVoiceSignalPacketAllowed(true, iceServers); + TMap servers = BungeeCord.getInstance().config.getServers(); + Set keySet = new HashSet(servers.keySet()); + keySet.removeAll(conf.getDisableVoiceOnServersSet()); + for(String s : keySet) { + serverMap.put(s, new VoiceServerImpl(servers.get(s), iceServersPacket)); + } + } + + public void handlePlayerLoggedIn(UserConnection player) { + + } + + public void handlePlayerLoggedOut(UserConnection player) { + + } + + public void handleServerConnected(UserConnection player, ServerInfo server) { + VoiceServerImpl svr = serverMap.get(server.getName()); + if(svr != null) { + svr.handlePlayerLoggedIn(player); + }else { + player.sendData(CHANNEL, disableVoicePacket); + } + } + + public void handleServerDisconnected(UserConnection player, ServerInfo server) { + VoiceServerImpl svr = serverMap.get(server.getName()); + if(svr != null) { + svr.handlePlayerLoggedOut(player); + } + } + + void handleVoiceSignalPacketTypeRequest(UUID player, UserConnection sender) { + if(sender.getServer() != null) { + VoiceServerImpl svr = serverMap.get(sender.getServer().getInfo().getName()); + if(svr != null) { + svr.handleVoiceSignalPacketTypeRequest(player, sender); + } + } + } + + void handleVoiceSignalPacketTypeConnect(UserConnection sender) { + if(sender.getServer() != null) { + VoiceServerImpl svr = serverMap.get(sender.getServer().getInfo().getName()); + if(svr != null) { + svr.handleVoiceSignalPacketTypeConnect(sender); + } + } + } + + void handleVoiceSignalPacketTypeICE(UUID player, String str, UserConnection sender) { + if(sender.getServer() != null) { + VoiceServerImpl svr = serverMap.get(sender.getServer().getInfo().getName()); + if(svr != null) { + svr.handleVoiceSignalPacketTypeICE(player, str, sender); + } + } + } + + void handleVoiceSignalPacketTypeDesc(UUID player, String str, UserConnection sender) { + if(sender.getServer() != null) { + VoiceServerImpl svr = serverMap.get(sender.getServer().getInfo().getName()); + if(svr != null) { + svr.handleVoiceSignalPacketTypeDesc(player, str, sender); + } + } + } + + void handleVoiceSignalPacketTypeDisconnect(UUID player, UserConnection sender) { + if(sender.getServer() != null) { + VoiceServerImpl svr = serverMap.get(sender.getServer().getInfo().getName()); + if(svr != null) { + svr.handleVoiceSignalPacketTypeDisconnect(player, sender); + } + } + } + +} diff --git a/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceSignalPackets.java b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceSignalPackets.java new file mode 100644 index 0000000..c9b2de9 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/voice/VoiceSignalPackets.java @@ -0,0 +1,194 @@ +package net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.voice; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.UUID; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.md_5.bungee.UserConnection; +import net.md_5.bungee.protocol.DefinedPacket; + +/** + * 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, UserConnection 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(DefinedPacket.readUUID(buffer), sender); + break; + } + case VOICE_SIGNAL_CONNECT: { + voiceService.handleVoiceSignalPacketTypeConnect(sender); + break; + } + case VOICE_SIGNAL_ICE: { + voiceService.handleVoiceSignalPacketTypeICE(DefinedPacket.readUUID(buffer), DefinedPacket.readString(buffer, 32767), sender); + break; + } + case VOICE_SIGNAL_DESC: { + voiceService.handleVoiceSignalPacketTypeDesc(DefinedPacket.readUUID(buffer), DefinedPacket.readString(buffer, 32767), sender); + break; + } + case VOICE_SIGNAL_DISCONNECT: { + voiceService.handleVoiceSignalPacketTypeDisconnect(buffer.readableBytes() > 0 ? DefinedPacket.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); + DefinedPacket.writeVarInt(iceServersBytes.length, wrappedBuffer); + for(int i = 0; i < iceServersBytes.length; ++i) { + byte[] b = iceServersBytes[i]; + DefinedPacket.writeVarInt(b.length, wrappedBuffer); + wrappedBuffer.writeBytes(b); + } + return ret; + } + + static byte[] makeVoiceSignalPacketGlobal(Collection users) { + int cnt = users.size(); + byte[][] displayNames = new byte[cnt][]; + int i = 0; + for(UserConnection user : users) { + String name = user.getDisplayName(); + 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); + DefinedPacket.writeVarInt(cnt, wrappedBuffer); + for(UserConnection user : users) { + DefinedPacket.writeUUID(user.getUniqueId(), wrappedBuffer); + } + for(i = 0; i < cnt; ++i) { + DefinedPacket.writeVarInt(displayNames[i].length, wrappedBuffer); + 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); + DefinedPacket.writeUUID(player, wrappedBuffer); + 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); + DefinedPacket.writeUUID(player, wrappedBuffer); + 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); + DefinedPacket.writeUUID(player, wrappedBuffer); + 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); + DefinedPacket.writeUUID(player, wrappedBuffer); + DefinedPacket.writeVarInt(strBytes.length, wrappedBuffer); + 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); + DefinedPacket.writeUUID(player, wrappedBuffer); + DefinedPacket.writeVarInt(strBytes.length, wrappedBuffer); + 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; + } +} diff --git a/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_authservice.yml b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_authservice.yml new file mode 100644 index 0000000..cdaa330 --- /dev/null +++ b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_authservice.yml @@ -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 \ No newline at end of file diff --git a/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_http_mime_types.json b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_http_mime_types.json new file mode 100644 index 0000000..38458bd --- /dev/null +++ b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_http_mime_types.json @@ -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 + } +} \ No newline at end of file diff --git a/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_ice_servers.yml b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_ice_servers.yml new file mode 100644 index 0000000..5e95a73 --- /dev/null +++ b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_ice_servers.yml @@ -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' \ No newline at end of file diff --git a/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_listeners.yml b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_listeners.yml new file mode 100644 index 0000000..894f62b --- /dev/null +++ b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_listeners.yml @@ -0,0 +1,66 @@ +listener_01: + address: 0.0.0.0:8081 + address_v6: 'null' + max_players: 60 + tab_list: GLOBAL_PING + default_server: lobby + force_default_server: false + 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' \ No newline at end of file diff --git a/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_settings.yml b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_settings.yml new file mode 100644 index 0000000..0be9f9c --- /dev/null +++ b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_settings.yml @@ -0,0 +1,25 @@ +server_name: 'EaglercraftXBungee 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: [] \ No newline at end of file diff --git a/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_updates.yml b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_updates.yml new file mode 100644 index 0000000..d8a8104 --- /dev/null +++ b/src/main/resources/net/lax1dude/eaglercraft/v1_8/plugin/gateway_bungeecord/config/default_updates.yml @@ -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 \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..0e0f9b4 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,5 @@ +name: EaglercraftXBungee +main: net.lax1dude.eaglercraft.v1_8.plugin.gateway_bungeecord.EaglerXBungee +version: 1.1.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 +author: lax1dude \ No newline at end of file