Compare commits

..
78 Commits
Author SHA1 Message Date
yoyosource 7a39ebb058 Fix StateRegistry and update to dev/4.0.0
SteamWarCI Build successful
2026-08-24 17:14:49 +02:00
yoyosource 57e9cd7f24 Merge remote-tracking branch 'upstream/dev/4.0.0'
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
2026-08-24 17:03:57 +02:00
Wouter Gritter 4772ca3022 Bump lmbda to 3.0.0, defining generated handlers with the proxy's lookup instead of the plugin's
Reapplies the lmbda 3.0.0 bump reverted in f918d0d6.
2026-08-14 12:53:49 +02:00
YoyoNow b9463125d1 Merge pull request 'Update26' (#3) from update26 into master
SteamWarCI Build successful
Reviewed-on: #3
2026-08-13 09:23:04 +02:00
Shane Freeder f918d0d649 Downgrade lmbda back to 2.0.0
lmbda 3.x moved to using hidden classes for generation which does
not work for cross classloader operations which are somewhat typical
for plugins.
2026-08-12 23:39:19 +01:00
Wouter GritterandGitHub e6fbcc9196 Various dependency bumps (#1860)
* Bump fastutil to 8.5.19 and remove exclusions

* Various dependency bumps
2026-08-12 18:32:24 +01:00
Radmir NoirusovandGitHub 14a69904f9 fix: retain reference-counted packets forwarded via handleGeneric (#1856)
During configuration, a ServerboundCustomClickActionPacket arriving
when connectionInFlight is null falls through to handleGeneric, which
writes it to the connected backend without retaining. The encoder
releases the packet, then MinecraftConnection.channelRead's finally
block releases again - double-free.

Two fixes:
- handle() now uses getConnectionInFlightOrConnectedServer() so the
  packet is properly retained before being written
- handleGeneric() retains any ByteBufHolder packet before write, not
  just PluginMessagePacket

Closes #1841
2026-08-12 18:29:30 +01:00
Jason PenillaandGitHub 71c50a75eb Export JSpecify annotations at runtime (#1861)
JSpecify annotations have runtime retention, so expose them through the API variant as recommended by JSpecify.
2026-08-12 18:25:15 +01:00
Shane Freeder 00759e5279 Revert "Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)"
This reverts commit 2676520c6a.
2026-08-03 14:09:05 +01:00
Phillipp W.andGitHub 06ade4775e fix: forward player loaded packet to backend server (#1862)
Return false after handling ServerboundPlayerLoadedPacket so Velocity's normal forwarding path sends the packet to the backend server.
2026-08-03 12:22:58 +01:00
Wouter Gritterandxphorror 2676520c6a Fix dimension reading for some mods that add extra dimensions in 1.7.10 (#1734)
Rewrite the comment and ternary operation to be clearer

Co-authored-by: xphorror <87706197+xphorror@users.noreply.github.com>
2026-08-02 11:13:25 +02:00
e11584ba35 Player Loaded World API (#1541)
Co-authored-by: Emil <12966472+Emilxyz@users.noreply.github.com>
Co-authored-by: Wouter Gritter <wouter@gritter.nl>
2026-07-31 20:03:22 +01:00
Shane Freeder a08972749b Rebuild natives 2026-07-30 16:51:07 +01:00
c6e9ca989e Add provides API (#1853)
Add provides API

Co-authored-by: Shane Freeder <theboyetronic@gmail.com>
2026-07-29 12:51:34 +01:00
Shane Freeder d30f1d9a74 Compressor cleanups 2026-07-29 00:51:41 +01:00
EmilandGitHub b45716deff feat: Make version clickable in velocity info command (#1775) 2026-07-19 16:02:20 +02:00
SpigotRCEandGitHub e653647962 [ci skip] typo fix (#1778) 2026-07-19 16:02:12 +02:00
Shane Freeder 5aab0d1427 [ci skip] primative and functional
oh, my! This stuff is not ideal, but it's the only real protection here unless
we just hack the automation here
2026-07-15 04:28:22 +01:00
Shane Freeder 1cd8d51d02 [ci skip] primative failsafe for mismatched version family 2026-07-15 02:29:29 +01:00
Shane Freeder 9eb338bd1c Fix version family 2026-07-15 02:22:41 +01:00
Andrew Steinborn 60380211f8 Defer ByteBufUtil.getBytes() in config stages until after we check for channel registry 2026-07-14 18:04:07 -04:00
Shane Freeder 3b239daf4c [ci skip] back to snapshots
Hello, Darkness, my old friend
2026-07-14 15:26:19 +01:00
Shane Freeder 90f89053a7 Release 4.0.0 2026-07-14 15:10:04 +01:00
Chaoscaot 67e16a308e Merge remote-tracking branch 'upstream/dev/4.0.0'
SteamWarCI Build successful
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java
2026-07-13 21:48:01 +02:00
Shane Freeder 4bd30bac33 Bump netty to 4.2.16.Final 2026-07-11 22:06:09 +01:00
BeanesandGitHub 28c9f5a356 Small optimization to prevent blocking netty threads on UUID.randomUUID() (#1781)
* Small optimization to prevent blocking netty threads on UUID.randomUUID()

* Change FastRandomUuid to be a valid uuid v4

* Update javadoc for spotless

* Migrate method to VelocityTabListLegacy and add notice that it is insecure

* Bring back deleted override
2026-07-11 22:09:17 +02:00
Wouter GritterandGitHub da7427fb51 Set TitleActionbarPacket's default action to SET_ACTION_BAR (#1802) 2026-07-11 22:09:11 +02:00
Wouter GritterandGitHub b8d1f16b07 Setup encryption before possible disconnect (#1754) 2026-07-11 22:09:03 +02:00
Wouter GritterandGitHub 729a050662 Fix build-time GraalVmProcessor warning (#1801) 2026-07-11 22:08:55 +02:00
Wouter GritterandGitHub c690b4abfc Strip pre-java-9 version check in Metrics as Velocity targets 21+ (#1836)
* Strip pre-java-9 version check in `Metrics` as Velocity targets 21+

* Simplify Java version check by using `Runtime.version()` (entry still needs the system property to produce the same bStats metrics)

* Reconstruct `java.version` system property through `Runtime.Version`

* Update `javaVersion()` javadoc

* Newline while we're here
2026-07-11 22:08:47 +02:00
Wouter GritterandGitHub 5cc6b0b7e5 Use SecureRandom to generate verification/anti-MITM token (#1797) 2026-07-11 22:08:36 +02:00
Wouter GritterandGitHub 757711a5bc Use correct max tab complete lengths for < 1.13, = 1.13 and > 1.13 (#1796) 2026-07-11 22:08:30 +02:00
Shane Freeder 95694cba28 Update jline to 4.3.1 2026-07-11 20:17:21 +01:00
yoyosource 4b8ceb5264 Fix UpdateTeamsPacket
SteamWarCI Build successful
2026-05-21 09:53:29 +02:00
yoyosource 2d1cade7bd Fix UpdateTeamsPacket
SteamWarCI Build successful
2026-05-21 09:19:40 +02:00
yoyosource 4067c44cd1 Fix UpdateTeamsPacket
SteamWarCI Build successful
2026-05-21 09:06:32 +02:00
yoyosource 45bb5de601 Fix Packet ID in 1.21.9 for UpdateTeamsPacket
SteamWarCI Build successful
2026-05-21 08:52:49 +02:00
yoyosource ebb9eb046d Fix UpdateTeamsPacket for 1.21.5+
SteamWarCI Build successful
2026-05-21 08:35:30 +02:00
Chaoscaot afc84b88ac Fix
SteamWarCI Build successful
2026-05-21 08:30:09 +02:00
Chaoscaot e6caa87865 Fix
SteamWarCI Build failed
2026-05-21 08:26:58 +02:00
Chaoscaot cdeabe91d0 Merge remote-tracking branch 'upstream/dev/3.0.0'
SteamWarCI Build failed
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
2026-05-21 08:23:49 +02:00
Chaoscaot babb22bf88 Remove duplicate mapping for MINECRAFT_1_21_2 in StateRegistry
SteamWarCI Build successful
2025-11-09 01:51:08 +01:00
Chaoscaot 468127996c Merge remote-tracking branch 'upstream/dev/3.0.0'
SteamWarCI Build failed
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
2025-11-09 01:46:27 +01:00
Chaoscaot 873fca763d Merge remote-tracking branch 'upstream/dev/3.0.0'
SteamWarCI Build successful
2025-07-28 18:34:52 +02:00
Chaoscaot 67d63faeca Reapply "Disable io_uring transport by default"
SteamWarCI Build successful
This reverts commit 11834de220.
2025-07-10 10:42:11 +02:00
Chaoscaot 871b053561 Merge remote-tracking branch 'upstream/dev/3.0.0' 2025-07-10 10:41:50 +02:00
Chaoscaot c2edc26d8e Merge pull request 'Update Velocity' (#2) from update into master
SteamWarCI Build successful
Reviewed-on: #2
2025-06-26 23:11:02 +02:00
Chaoscaot 76417b13d4 Merge branch 'updatev2' into update
SteamWarCI Build successful
2025-06-26 22:53:32 +02:00
Chaoscaot 47f36e3ff9 Merge remote-tracking branch 'upstream/dev/3.0.0' into updatev2
SteamWarCI Build successful
2025-06-26 22:52:31 +02:00
Chaoscaot 75bb48d00e Merge remote-tracking branch 'upstream/dev/3.0.0' into updatev2
SteamWarCI Build successful
2025-06-03 23:12:58 +02:00
Chaoscaot 11834de220 Revert "Disable io_uring transport by default"
SteamWarCI Build successful
This reverts commit ae312339a3.
2025-05-02 20:49:11 +02:00
Chaoscaot 91a61643bd Revert "Disable io_uring transport by default"
SteamWarCI Build successful
This reverts commit ae312339a3.
2025-04-27 20:24:41 +02:00
Chaoscaot b6e05cb0b9 Refactor TCP Fast Open checks and update message identifiers.
SteamWarCI Build successful
Removed transport type conditions for TCP Fast Open to streamline configuration usage. Added imports for new message identifiers in `ClientPlaySessionHandler`. Cleaned up Netty library definitions in `libs.versions.toml`.
2025-04-27 20:09:05 +02:00
Chaoscaot 1507b91463 Merge remote-tracking branch 'upstream/dev/3.0.0' into update
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java
2025-04-27 19:53:15 +02:00
Lixfel b06af3718c Merge remote-tracking branch 'github/dev/3.0.0'
SteamWarCI Build successful
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java
2025-04-01 07:06:14 +02:00
Lixfel a20a896582 Skip javadoc generation
SteamWarCI Build successful
2025-01-22 09:37:30 +01:00
Lixfel e1a3421212 Adapt to new server
SteamWarCI Build failed
2025-01-22 09:33:59 +01:00
Lixfel 19e51a2b12 Merge remote-tracking branch 'upstream/dev/3.0.0' 2024-12-06 11:14:27 +01:00
Lixfel b89a5c5ce9 Fix CI 2024-12-02 12:45:04 +01:00
Lixfel 65d3277319 Merge remote-tracking branch 'upstream/dev/3.0.0' 2024-11-30 09:25:25 +01:00
Lixfel a22bfa10f9 Merge pull request 'Update 1.21.2 client support' (#5) from upstream into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Velocity/pulls/5
2024-11-11 08:15:32 +01:00
Lixfel d9d1319a3a Merge remote-tracking branch 'upstream/dev/3.0.0' into upstream
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
2024-11-10 18:32:48 +01:00
Lixfel 15ecbf4345 Merge pull request 'Update Velocity (might fix Command problems) and fix PluginMessages...' (#4) from fix-pluginmessages into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Velocity/pulls/4
Reviewed-by: YoyoNow <jwsteam@nidido.de>
2024-08-20 08:14:11 +02:00
Lixfel 5e3bbcd427 Fix command signature issues. 2024-08-20 08:03:18 +02:00
Lixfel a6c79db07b Remove filter checks to receive PluginMessages unfiltered. 2024-08-18 15:32:08 +02:00
Lixfel 6e33bc6c17 Merge remote-tracking branch 'refs/remotes/upstream/dev/3.0.0' 2024-08-18 15:28:10 +02:00
Lixfel 01208bb359 Indicate NoChatReports support in ServerPing 2024-06-24 18:36:32 +02:00
Lixfel fa88aaae52 Always unsign chat. 2024-06-21 12:48:58 +02:00
Lixfel 2da400a267 Merge pull request 'Implement Velocity PRs #998 #1246 and #1309 (io_uring, tcp_fastopen and PluginMessage race condition fix)' (#3) from io_uring into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Velocity/pulls/3
Reviewed-by: YoyoNow <jwsteam@nidido.de>
2024-06-19 12:06:10 +02:00
Lixfel 8103135dfb Fix type 2024-06-19 10:21:36 +02:00
Lixfel cfabff7288 Implement Velocity PRs #998 #1246 and #1309 (io_uring, tcp_fastopen and PluginMessage race condition fix) 2024-06-19 10:11:32 +02:00
Lixfel 2f5a27a708 Fix CI 2024-06-19 09:47:18 +02:00
Lixfel fdfe8bcc4b Fix CI 2024-06-19 09:40:58 +02:00
Chaoscaot a19fd8db74 Add UpdateTeamsPacket 2024-06-16 21:27:13 +02:00
Chaoscaot e63d71423d Add UpdateTeamsPacket 2024-06-16 21:24:06 +02:00
Lixfel a7afe35fab Rebuild 2024-06-16 13:25:07 +02:00
Lixfel 56d6339313 Fix JVM 2024-06-16 13:18:29 +02:00
Lixfel 2475572573 Add steamwarci.yml 2024-06-16 12:52:43 +02:00
52 changed files with 889 additions and 227 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ java {
} }
dependencies { dependencies {
compileOnlyApi(libs.jspecify) api(libs.jspecify)
api(libs.gson) api(libs.gson)
api(libs.guava) api(libs.guava)
@@ -97,6 +97,16 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
} }
} }
for (String provided : plugin.provides()) {
if (!SerializedPluginDescription.ID_PATTERN.matcher(provided).matches()) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Invalid provided ID '" + provided + "' for plugin " + qualifiedName
+ ". IDs must start alphabetically, have lowercase alphanumeric characters, and "
+ "can contain dashes or underscores.");
return false;
}
}
// All good, generate the velocity-plugin.json. // All good, generate the velocity-plugin.json.
SerializedPluginDescription description = SerializedPluginDescription SerializedPluginDescription description = SerializedPluginDescription
.from(plugin, qualifiedName.toString()); .from(plugin, qualifiedName.toString());
@@ -35,11 +35,12 @@ public final class SerializedPluginDescription {
private final @Nullable String url; private final @Nullable String url;
private final @Nullable List<String> authors; private final @Nullable List<String> authors;
private final @Nullable List<Dependency> dependencies; private final @Nullable List<Dependency> dependencies;
private final @Nullable List<String> provides;
private final String main; private final String main;
private SerializedPluginDescription(String id, String name, String version, String description, private SerializedPluginDescription(String id, String name, String version, String description,
String url, String url,
List<String> authors, List<Dependency> dependencies, String main) { List<String> authors, List<Dependency> dependencies, List<String> provides, String main) {
Preconditions.checkNotNull(id, "id"); Preconditions.checkNotNull(id, "id");
Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid"); Preconditions.checkArgument(ID_PATTERN.matcher(id).matches(), "id is not valid");
this.id = id; this.id = id;
@@ -50,6 +51,7 @@ public final class SerializedPluginDescription {
this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors; this.authors = authors == null || authors.isEmpty() ? ImmutableList.of() : authors;
this.dependencies = this.dependencies =
dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies; dependencies == null || dependencies.isEmpty() ? ImmutableList.of() : dependencies;
this.provides = provides == null || provides.isEmpty() ? ImmutableList.of() : provides;
this.main = Preconditions.checkNotNull(main, "main"); this.main = Preconditions.checkNotNull(main, "main");
} }
@@ -61,7 +63,9 @@ public final class SerializedPluginDescription {
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(),
plugin.description(), plugin.url(), plugin.description(), plugin.url(),
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty()) Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty())
.collect(Collectors.toList()), dependencies, qualifiedName); .collect(Collectors.toList()), dependencies,
Arrays.stream(plugin.provides()).filter(provided -> !provided.isEmpty())
.collect(Collectors.toList()), qualifiedName);
} }
public String getId() { public String getId() {
@@ -92,6 +96,10 @@ public final class SerializedPluginDescription {
return dependencies == null ? ImmutableList.of() : dependencies; return dependencies == null ? ImmutableList.of() : dependencies;
} }
public List<String> getProvides() {
return provides == null ? ImmutableList.of() : provides;
}
public String getMain() { public String getMain() {
return main; return main;
} }
@@ -112,12 +120,13 @@ public final class SerializedPluginDescription {
&& Objects.equals(url, that.url) && Objects.equals(url, that.url)
&& Objects.equals(authors, that.authors) && Objects.equals(authors, that.authors)
&& Objects.equals(dependencies, that.dependencies) && Objects.equals(dependencies, that.dependencies)
&& Objects.equals(provides, that.provides)
&& Objects.equals(main, that.main); && Objects.equals(main, that.main);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(id, name, version, description, url, authors, dependencies); return Objects.hash(id, name, version, description, url, authors, dependencies, provides);
} }
@Override @Override
@@ -130,6 +139,7 @@ public final class SerializedPluginDescription {
+ ", url='" + url + '\'' + ", url='" + url + '\''
+ ", authors=" + authors + ", authors=" + authors
+ ", dependencies=" + dependencies + ", dependencies=" + dependencies
+ ", provides=" + provides
+ ", main='" + main + '\'' + ", main='" + main + '\''
+ '}'; + '}';
} }
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2018-2026 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/
package com.velocitypowered.api.event.player;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
/**
* Called when a player is marked as loaded by the client.
*
* <p>This event is fired once per {@link com.velocitypowered.api.proxy.ServerConnection}
* when the player explicitly notifies the server after loading the world (closing the downloading terrain screen)
*
* @implNote Unlike Paper this event will <u>not</u> fire due to a timeout nor respawning.
* Though plugins can implement a timeout by scheduling a task in {@link ServerPostConnectEvent}
* and checking {@link com.velocitypowered.api.proxy.ServerConnection#isClientLoaded()}.
* @sinceMinecraft 1.21.4
* @since 4.1.0
*/
@Beta
public final class PlayerClientLoadedWorldEvent {
private final Player player;
public PlayerClientLoadedWorldEvent(Player player) {
this.player = Preconditions.checkNotNull(player, "player");
}
public Player getPlayer() {
return player;
}
@Override
public String toString() {
return "PlayerClientLoadedWorldEvent{"
+ "player=" + player
+ '}';
}
}
@@ -72,4 +72,12 @@ public @interface Plugin {
* @return the plugin dependencies * @return the plugin dependencies
*/ */
Dependency[] dependencies() default {}; Dependency[] dependencies() default {};
/**
* The plugin IDs this plugin "provides" for. Each ID must match
* {@link SerializedPluginDescription#ID_PATTERN_STRING}.
*
* @return the provided IDs
*/
String[] provides() default {};
} }
@@ -100,6 +100,16 @@ public interface PluginDescription {
return Optional.empty(); return Optional.empty();
} }
/**
* Gets a {@link Collection} of the provided IDs of the {@link Plugin} within this container.
*
* @return the provided plugins IDs, can be empty
* @see Plugin#provides()
*/
default Collection<String> getProvidedIds() {
return ImmutableSet.of();
}
/** /**
* Returns the source the plugin was loaded from. * Returns the source the plugin was loaded from.
* *
@@ -7,6 +7,7 @@
package com.velocitypowered.api.proxy; package com.velocitypowered.api.proxy;
import com.google.common.annotations.Beta;
import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
@@ -40,6 +41,17 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi
*/ */
ServerInfo getServerInfo(); ServerInfo getServerInfo();
/**
* Returns whether the client notified this connection of having loaded the world.
*
* @return true if the client has loaded the world
* @implNote This is purely client-dependent; see {@link com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent}.
* @sinceMinecraft 1.21.4
* @since 4.1.0
*/
@Beta
boolean isClientLoaded();
/** /**
* Returns the player that this connection is associated with. * Returns the player that this connection is associated with.
* *
@@ -33,6 +33,7 @@ public final class ServerPing {
private final @Nullable Component description; private final @Nullable Component description;
private final @Nullable Favicon favicon; private final @Nullable Favicon favicon;
private final @Nullable ModInfo modinfo; private final @Nullable ModInfo modinfo;
private final boolean preventsChatReports = true;
public ServerPing(Version version, @Nullable Players players, public ServerPing(Version version, @Nullable Players players,
net.kyori.adventure.text.Component description, @Nullable Favicon favicon) { net.kyori.adventure.text.Component description, @Nullable Favicon favicon) {
@@ -32,5 +32,6 @@ tasks.withType<Jar> {
archiveVersion.get() archiveVersion.get()
} }
attributes["Implementation-Version"] = velocityHumanVersion attributes["Implementation-Version"] = velocityHumanVersion
attributes["Enable-Native-Access"] = "ALL-UNNAMED"
} }
} }
+1 -1
View File
@@ -1,2 +1,2 @@
group=com.velocitypowered group=com.velocitypowered
version=4.0.0-SNAPSHOT version=4.1.0-SNAPSHOT
+18 -18
View File
@@ -2,24 +2,24 @@
configurate3 = "3.7.3" configurate3 = "3.7.3"
configurate4 = "4.2.0" configurate4 = "4.2.0"
flare = "2.0.1" flare = "2.0.1"
log4j = "2.26.0" log4j = "2.26.1"
netty = "4.2.15.Final" netty = "4.2.16.Final"
[plugins] [plugins]
fill = "io.papermc.fill.gradle:1.0.12" fill = "io.papermc.fill.gradle:1.0.12"
shadow = "com.gradleup.shadow:9.5.1" shadow = "com.gradleup.shadow:9.6.1"
spotless = "com.diffplug.spotless:8.2.0" spotless = "com.diffplug.spotless:8.9.0"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom:5.2.0" adventure-bom = "net.kyori:adventure-bom:5.2.0"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0" adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0"
asm = "org.ow2.asm:asm:9.9.1" asm = "org.ow2.asm:asm:9.10.1"
auto-service = "com.google.auto.service:auto-service:1.1.1" auto-service = "com.google.auto.service:auto-service:1.1.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.1.0" bstats = "org.bstats:bstats-base:3.2.1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3" caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.4"
checker-qual = "org.checkerframework:checker-qual:3.53.0" checker-qual = "org.checkerframework:checker-qual:4.2.1"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
completablefutures = "com.spotify:completable-futures:0.3.6" completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" } configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
@@ -29,24 +29,24 @@ configurate4-hocon = { module = "org.spongepowered:configurate-hocon", version.r
configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" } configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" }
configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" } configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" }
disruptor = "com.lmax:disruptor:4.0.0" disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.18" fastutil = "it.unimi.dsi:fastutil:8.5.19"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" } flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" } flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
jline = "org.jline:jline-terminal-jansi:3.30.6" jline = "org.jline:jline-terminal-ffm:4.3.1"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:6.0.3" junit = "org.junit.jupiter:junit-jupiter:6.1.2"
jspecify = "org.jspecify:jspecify:1.0.0" jspecify = "org.jspecify:jspecify:1.0.1"
kyori-ansi = "net.kyori:ansi:1.1.1" kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:33.6.0-jre" guava = "com.google.guava:guava:33.6.0-jre"
gson = "com.google.code.gson:gson:2.14.0" gson = "com.google.code.gson:gson:2.14.0"
guice = "com.google.inject:guice:7.0.0" guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:2.0.0" lmbda = "org.lanternpowered:lmbda:3.0.0"
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" } log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" }
log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" }
mockito = "org.mockito:mockito-core:5.22.0" mockito = "org.mockito:mockito-core:5.23.0"
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" } netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" } netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" }
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" } netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
@@ -54,10 +54,10 @@ netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" } netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" } netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.8.3" nightconfig = "com.electronwill.night-config:toml:3.9.0"
slf4j = "org.slf4j:slf4j-api:2.0.17" slf4j = "org.slf4j:slf4j-api:2.0.18"
snakeyaml = "org.yaml:snakeyaml:2.5" snakeyaml = "org.yaml:snakeyaml:2.6"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.9.8" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.10.3"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles] [bundles]
@@ -5,29 +5,44 @@ set -e
# make sure we're in the correct directory - the top-level `native` directory # make sure we're in the correct directory - the top-level `native` directory
cd "$(dirname "$0")/.." || exit 1 cd "$(dirname "$0")/.." || exit 1
# Use docker by default, falling back to podman. Set CONTAINER_ENGINE to pick one explicitly.
if [ -z "$CONTAINER_ENGINE" ]; then
if command -v docker > /dev/null 2>&1; then
CONTAINER_ENGINE=docker
elif command -v podman > /dev/null 2>&1; then
CONTAINER_ENGINE=podman
else
echo "Neither docker nor podman was found on PATH." >&2
exit 1
fi
fi
echo "Using container engine: $CONTAINER_ENGINE"
ARCHS=(x86_64 aarch64) ARCHS=(x86_64 aarch64)
BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine) BASE_DOCKERFILE_VARIANTS=(ubuntu-focal ubuntu-jammy alpine)
COMPRESSION_VARIANTS=(ubuntu-focal alpine) COMPRESSION_VARIANTS=(ubuntu-focal alpine)
# Build one image per (variant, arch). Passing several --platform flags to a single tagged build
# does not produce a multi-arch tag - only the last architecture keeps the tag, and the run below
# then sees a platform mismatch, treats the image as missing and tries to pull it from a registry.
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
docker_platforms=""
for arch in "${ARCHS[@]}"; do for arch in "${ARCHS[@]}"; do
docker_platforms="$docker_platforms --platform linux/${arch}" echo "Building base build image for $variant on $arch..."
$CONTAINER_ENGINE build -t velocity-native-build:$variant-$arch --platform linux/${arch} \
-f build-support/$variant.Dockerfile .
done done
echo "Building base build image for $variant..."
docker build -t velocity-native-build:$variant $docker_platforms -f build-support/$variant.Dockerfile .
done done
for arch in "${ARCHS[@]}"; do for arch in "${ARCHS[@]}"; do
for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do for variant in "${BASE_DOCKERFILE_VARIANTS[@]}"; do
echo "Building native crypto for $arch on $variant..." echo "Building native crypto for $arch on $variant..."
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh" $CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-crypto.sh"
done done
for variant in "${COMPRESSION_VARIANTS[@]}"; do for variant in "${COMPRESSION_VARIANTS[@]}"; do
echo "Building native compression for $arch on $variant..." echo "Building native compression for $arch on $variant..."
docker run --rm -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh" $CONTAINER_ENGINE run --rm --pull=never -v "$(pwd)":/app --platform linux/${arch} velocity-native-build:$variant-$arch /bin/bash -c "cd /app && ./build-support/compile-linux-compress.sh"
done done
done done
+1 -2
View File
@@ -34,8 +34,7 @@ Java_com_velocitypowered_natives_compression_NativeZlibInflate_process(JNIEnv *e
jlong sourceAddress, jlong sourceAddress,
jint sourceLength, jint sourceLength,
jlong destinationAddress, jlong destinationAddress,
jint destinationLength, jint destinationLength)
jlong maximumSize)
{ {
struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx; struct libdeflate_decompressor *decompress = (struct libdeflate_decompressor *) ctx;
enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress, enum libdeflate_result result = libdeflate_zlib_decompress(decompress, (void *) sourceAddress,
@@ -56,24 +56,45 @@ public class JavaVelocityCompressor implements VelocityCompressor {
final int origIdx = source.readerIndex(); final int origIdx = source.readerIndex();
inflater.setInput(source.nioBuffer()); inflater.setInput(source.nioBuffer());
int totalProduced = 0;
try { try {
final int readable = source.readableBytes(); final int readable = source.readableBytes();
while (!inflater.finished() && inflater.getBytesRead() < readable) { while (!inflater.finished() && inflater.getBytesRead() < readable) {
if (totalProduced >= uncompressedSize) {
throw new DataFormatException("Decompressed data exceeds the claimed uncompressed size "
+ "of " + uncompressedSize + " bytes");
}
final int remaining = uncompressedSize - totalProduced;
if (!destination.isWritable()) { if (!destination.isWritable()) {
destination.ensureWritable(ZLIB_BUFFER_SIZE); destination.ensureWritable(Math.min(ZLIB_BUFFER_SIZE, remaining));
} }
ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(), ByteBuffer destNioBuf = destination.nioBuffer(destination.writerIndex(),
destination.writableBytes()); destination.writableBytes());
// Never let a single inflate step write past the claimed size
if (destNioBuf.remaining() > remaining) {
destNioBuf.limit(destNioBuf.position() + remaining);
}
int produced = inflater.inflate(destNioBuf); int produced = inflater.inflate(destNioBuf);
if (produced == 0 && !inflater.finished()) {
// Output space was available yet the inflater made no progress: the stream is truncated
// or corrupt (this also covers a peer that over-reported the uncompressed size).
throw new DataFormatException("Received a truncated or malformed deflate stream, "
+ "expected " + uncompressedSize + " bytes");
}
totalProduced += produced;
destination.writerIndex(destination.writerIndex() + produced); destination.writerIndex(destination.writerIndex() + produced);
} }
if (!inflater.finished()) { if (!inflater.finished()) {
throw new DataFormatException("Received a deflate stream that was too large, wanted " throw new DataFormatException("Received a truncated or malformed deflate stream, expected "
+ uncompressedSize); + uncompressedSize + " bytes");
} }
source.readerIndex(origIdx + inflater.getTotalIn());
source.readerIndex(origIdx + (int) inflater.getBytesRead());
} finally { } finally {
inflater.reset(); inflater.reset();
} }
@@ -102,7 +123,7 @@ public class JavaVelocityCompressor implements VelocityCompressor {
destination.writerIndex(destination.writerIndex() + produced); destination.writerIndex(destination.writerIndex() + produced);
} }
source.readerIndex(origIdx + deflater.getTotalIn()); source.readerIndex(origIdx + (int) deflater.getBytesRead());
deflater.reset(); deflater.reset();
} }
@@ -17,6 +17,7 @@
package com.velocitypowered.natives.compression; package com.velocitypowered.natives.compression;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@@ -77,6 +78,83 @@ class VelocityCompressorTest {
check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32)); check(compressor, () -> Unpooled.buffer(TEST_DATA.length + 32));
} }
private static final int BOMB_ACTUAL_SIZE = 1 << 20;
private static final int BOMB_LYING_CLAIM = 1024;
@Test
void javaRejectsUnderReportedUncompressedSize() throws DataFormatException {
VelocityCompressor compressor = JavaVelocityCompressor.FACTORY
.create(Deflater.DEFAULT_COMPRESSION);
DataFormatException ex = assertRejectsDecompressionBomb(compressor);
// The Java compressor's size guard names the claimed size in its message, so operators can
// tell an over-size rejection apart from a genuinely corrupt stream.
assertTrue(ex.getMessage().contains(String.valueOf(BOMB_LYING_CLAIM)),
"rejection must originate from the uncompressed-size guard, got: " + ex.getMessage());
}
@Test
@EnabledOnOs({LINUX})
void nativeRejectsUnderReportedUncompressedSize() throws DataFormatException {
VelocityCompressor compressor = Natives.compress.get().create(Deflater.DEFAULT_COMPRESSION);
if (compressor.preferredBufferType() != BufferPreference.DIRECT_REQUIRED) {
compressor.close();
fail("Loaded regular compressor");
}
// libdeflate rejects with its own native-origin message ("uncompressed size is inaccurate"),
// so we only assert the behavioural guarantee here, not the message text.
assertRejectsDecompressionBomb(compressor);
}
/**
* Asserts that a compressor refuses a decompression bomb: a small, valid deflate stream whose
* claimed uncompressed size is far smaller than what it actually inflates to. Verifies the same
* stream round-trips when the claimed size is honest (proving the rejection is caused by the
* under-reported size, not corrupt input) and that no output is written past the claimed size.
* Closes the compressor before returning the exception thrown by the rejected inflate.
*/
private DataFormatException assertRejectsDecompressionBomb(VelocityCompressor compressor)
throws DataFormatException {
// Direct buffers so this works for the native compressor, which requires them.
ByteBuf source = Unpooled.directBuffer(BOMB_ACTUAL_SIZE);
ByteBuf compressed = Unpooled.directBuffer();
try {
source.writeZero(BOMB_ACTUAL_SIZE);
compressor.deflate(source, compressed);
final int compressedSize = compressed.readableBytes();
assertTrue(compressedSize < BOMB_ACTUAL_SIZE / 100,
"sanity: payload really is a decompression bomb (" + compressedSize + " -> "
+ BOMB_ACTUAL_SIZE + ")");
// Positive control: the compressed stream is perfectly valid and round-trips when the peer
// tells the truth about its uncompressed size.
ByteBuf honest = Unpooled.directBuffer();
try {
compressor.inflate(compressed.duplicate(), honest, BOMB_ACTUAL_SIZE);
assertEquals(BOMB_ACTUAL_SIZE, honest.readableBytes(),
"valid stream must fully decompress when the claimed size is honest");
} finally {
honest.release();
}
// Attack: same valid stream, but a tiny claimed size. inflate must refuse rather than grow
// the destination without bound.
ByteBuf decompressed = Unpooled.directBuffer();
try {
DataFormatException ex = assertThrows(DataFormatException.class,
() -> compressor.inflate(compressed.duplicate(), decompressed, BOMB_LYING_CLAIM));
assertTrue(decompressed.writerIndex() <= BOMB_LYING_CLAIM,
"inflate must not write past the claimed uncompressed size");
return ex;
} finally {
decompressed.release();
}
} finally {
source.release();
compressed.release();
compressor.close();
}
}
private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier) private void check(VelocityCompressor compressor, Supplier<ByteBuf> bufSupplier)
throws DataFormatException { throws DataFormatException {
ByteBuf source = bufSupplier.get(); ByteBuf source = bufSupplier.get();
+14 -60
View File
@@ -33,65 +33,6 @@ tasks {
transform(Log4j2PluginsCacheFileTransformer::class.java) transform(Log4j2PluginsCacheFileTransformer::class.java)
// Exclude all the collection types we don"t intend to use
exclude("it/unimi/dsi/fastutil/booleans/**")
exclude("it/unimi/dsi/fastutil/bytes/**")
exclude("it/unimi/dsi/fastutil/chars/**")
exclude("it/unimi/dsi/fastutil/doubles/**")
exclude("it/unimi/dsi/fastutil/floats/**")
exclude("it/unimi/dsi/fastutil/longs/**")
exclude("it/unimi/dsi/fastutil/shorts/**")
// Exclude the fastutil IO utilities - we don"t use them.
exclude("it/unimi/dsi/fastutil/io/**")
// Exclude most of the int types - Object2IntMap have a values() method that returns an
// IntCollection, and we need Int2ObjectMap
exclude("it/unimi/dsi/fastutil/ints/*Int2Boolean*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Byte*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Char*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Double*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Float*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Int*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Long*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Short*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*")
exclude("it/unimi/dsi/fastutil/ints/IntAVL*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayF*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayI*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayL*")
exclude("it/unimi/dsi/fastutil/ints/IntArrayP*")
exclude("it/unimi/dsi/fastutil/ints/IntArraySet*")
exclude("it/unimi/dsi/fastutil/ints/*IntBi*")
exclude("it/unimi/dsi/fastutil/ints/Int*Pair")
exclude("it/unimi/dsi/fastutil/ints/IntLinked*")
exclude("it/unimi/dsi/fastutil/ints/IntList*")
exclude("it/unimi/dsi/fastutil/ints/IntHeap*")
exclude("it/unimi/dsi/fastutil/ints/IntOpen*")
exclude("it/unimi/dsi/fastutil/ints/IntRB*")
exclude("it/unimi/dsi/fastutil/ints/IntSorted*")
exclude("it/unimi/dsi/fastutil/ints/*Priority*")
exclude("it/unimi/dsi/fastutil/ints/*BigList*")
// Try to exclude everything BUT Object2Int{LinkedOpen,Open,CustomOpen}HashMap
exclude("it/unimi/dsi/fastutil/objects/*ObjectArray*")
exclude("it/unimi/dsi/fastutil/objects/*ObjectAVL*")
exclude("it/unimi/dsi/fastutil/objects/*Object*Big*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Boolean*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Byte*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Char*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Double*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Float*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntArray*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntAVL*")
exclude("it/unimi/dsi/fastutil/objects/*Object2IntRB*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Long*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Object*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Reference*")
exclude("it/unimi/dsi/fastutil/objects/*Object2Short*")
exclude("it/unimi/dsi/fastutil/objects/*ObjectRB*")
exclude("it/unimi/dsi/fastutil/objects/*Reference*")
// Exclude Checker Framework annotations // Exclude Checker Framework annotations
exclude("org/checkerframework/checker/**") exclude("org/checkerframework/checker/**")
@@ -112,6 +53,15 @@ tasks {
workingDir = file("run").also(File::mkdirs) workingDir = file("run").also(File::mkdirs)
standardInput = System.`in` // Doesn't work? standardInput = System.`in` // Doesn't work?
} }
withType<JavaCompile>().configureEach {
options.compilerArgs.addAll(
listOf(
"-Alog4j.graalvm.groupId=${project.group}",
"-Alog4j.graalvm.artifactId=${project.name}"
)
)
}
} }
val projectVersion = version as String val projectVersion = version as String
@@ -120,9 +70,13 @@ fill {
build { build {
channel = BuildChannel.STABLE channel = BuildChannel.STABLE
versionFamily("3.0.0") versionFamily("4.0.0")
version(projectVersion) version(projectVersion)
if (versionFamily.get().split(".")[0] != projectVersion.split(".")[0]) {
throw IllegalArgumentException("Version family does not match project version")
}
downloads { downloads {
register("server:default") { register("server:default") {
file = tasks.shadowJar.flatMap { it.archiveFile } file = tasks.shadowJar.flatMap { it.archiveFile }
@@ -21,10 +21,8 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher; import java.util.stream.Collectors;
import java.util.regex.Pattern;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bstats.MetricsBase; import org.bstats.MetricsBase;
@@ -120,38 +118,28 @@ public class Metrics {
() -> server.getVersion().getVersion())); () -> server.getVersion().getVersion()));
metrics.addCustomChart(new DrilldownPie("java_version", () -> { metrics.addCustomChart(new DrilldownPie("java_version", () -> {
Map<String, Map<String, Integer>> map = new HashMap<>(); Runtime.Version version = Runtime.version();
String javaVersion = System.getProperty("java.version");
Map<String, Integer> entry = new HashMap<>();
entry.put(javaVersion, 1);
// http://openjdk.java.net/jeps/223 return Map.of(
// Java decided to change their versioning scheme and in doing so modified the "Java " + version.feature(),
// java.version system property to return $major[.$minor][.$security][-ea], as opposed to Map.of(javaVersion(version), 1));
// 1.$major.0_$identifier we can handle pre-9 by checking if the "major" is equal to "1",
// otherwise, 9+
String majorVersion = javaVersion.split("\\.")[0];
String release;
int indexOf = javaVersion.lastIndexOf('.');
if (majorVersion.equals("1")) {
release = "Java " + javaVersion.substring(0, indexOf);
} else {
// of course, it really wouldn't be all that simple if they didn't add a quirk, now
// would it valid strings for the major may potentially include values such as -ea to
// denote a pre release
Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion);
if (versionMatcher.find()) {
majorVersion = versionMatcher.group(0);
}
release = "Java " + majorVersion;
}
map.put(release, entry);
return map;
})); }));
} }
} }
/**
* Recreates the exact {@code java.version} system property value from a {@link Runtime.Version}.
*
* <p>Per <a href="https://openjdk.org/jeps/223">JEP 223</a>, {@code java.version} is
* {@code $VNUM(-$PRE)?}; the build and optional segments only appear in {@code java.runtime.version}.
*
* @param v the runtime version
* @return the value {@code java.version} would hold on this JVM
*/
private static String javaVersion(Runtime.Version v) {
return v.version().stream()
.map(Object::toString)
.collect(Collectors.joining("."))
+ v.pre().map(p -> "-" + p).orElse("");
}
} }
@@ -221,7 +221,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
PluginDescription description = new VelocityPluginDescription( PluginDescription description = new VelocityPluginDescription(
"velocity", version.getName(), version.getVersion(), "The Velocity proxy", "velocity", version.getName(), version.getVersion(), "The Velocity proxy",
version.getName().equals("Velocity") ? VELOCITY_URL : null, version.getName().equals("Velocity") ? VELOCITY_URL : null,
ImmutableList.of(version.getVendor()), Collections.emptyList(), null); ImmutableList.of(version.getVendor()), Collections.emptyList(),
Collections.emptyList(), null);
VelocityPluginContainer container = new VelocityPluginContainer(description); VelocityPluginContainer container = new VelocityPluginContainer(description);
container.setInstance(VelocityVirtualPlugin.INSTANCE); container.setInstance(VelocityVirtualPlugin.INSTANCE);
return container; return container;
@@ -163,6 +163,9 @@ public final class VelocityCommand {
.append(Component.text() .append(Component.text()
.content(version.getVersion()) .content(version.getVersion())
.decoration(TextDecoration.BOLD, false)) .decoration(TextDecoration.BOLD, false))
.hoverEvent(Component.translatable("velocity.command.version-offer-copy-version"))
.clickEvent(ClickEvent.copyToClipboard(version.getName() + " "
+ version.getVersion()))
.build(); .build();
final Component copyright = Component final Component copyright = Component
.translatable("velocity.command.version-copyright", .translatable("velocity.command.version-copyright",
@@ -53,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -200,6 +201,10 @@ public interface MinecraftSessionHandler {
return false; return false;
} }
default boolean handle(ServerboundPlayerLoadedPacket packet) {
return false;
}
default boolean handle(ServerLoginPacket packet) { default boolean handle(ServerLoginPacket packet) {
return false; return false;
} }
@@ -34,6 +34,8 @@ import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.command.CommandGraphInjector; import com.velocitypowered.proxy.command.CommandGraphInjector;
@@ -294,7 +296,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
// Register and unregister packets are simply forwarded to the server as-is. // Register and unregister packets are simply forwarded to the client as-is.
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) { if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
return false; return false;
} }
@@ -312,13 +314,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
return false;
}
byte[] copy = ByteBufUtil.getBytes(packet.content()); byte[] copy = ByteBufUtil.getBytes(packet.content());
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy); String channel = packet.getChannel();
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
server.getEventManager().fire(event).thenAcceptAsync(pme -> { server.getEventManager().fire(event).thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
PluginMessagePacket copied = new PluginMessagePacket( PluginMessagePacket copied = new PluginMessagePacket(
@@ -277,7 +277,6 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
serverConn.getPlayer().getProtocolVersion())); serverConn.getPlayer().getProtocolVersion()));
} else { } else {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@@ -287,6 +286,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
// Handling this stuff async means that we should probably pause // Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool // the connection while we toss this off into another pool
byte[] bytes = ByteBufUtil.getBytes(packet.content());
this.serverConn.getConnection().setAutoReading(false); this.serverConn.getConnection().setAutoReading(false);
this.server.getEventManager() this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes)) .fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
@@ -68,6 +68,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final VelocityServer server; private final VelocityServer server;
private @Nullable MinecraftConnection connection; private @Nullable MinecraftConnection connection;
private boolean hasCompletedJoin = false; private boolean hasCompletedJoin = false;
private boolean clientLoaded = false; // 1.21.4+
private boolean gracefulDisconnect = false; private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>(); private final Map<Long, Long> pendingPings = new HashMap<>();
@@ -317,6 +318,15 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
} }
} }
public void setClientLoaded(boolean clientLoaded) {
this.clientLoaded = clientLoaded;
}
@Override
public boolean isClientLoaded() {
return clientLoaded;
}
boolean isGracefulDisconnect() { boolean isGracefulDisconnect() {
return gracefulDisconnect; return gracefulDisconnect;
} }
@@ -47,6 +47,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -135,7 +136,6 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
} else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
return true; return true;
} else if (serverConn != null) { } else if (serverConn != null) {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@@ -145,6 +145,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
// Handling this stuff async means that we should probably pause // Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool // the connection while we toss this off into another pool
byte[] bytes = ByteBufUtil.getBytes(packet.content());
serverConn.getPlayer().getConnection().setAutoReading(false); serverConn.getPlayer().getConnection().setAutoReading(false);
this.server.getEventManager() this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes)) .fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
@@ -212,8 +213,9 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(ServerboundCustomClickActionPacket packet) { public boolean handle(ServerboundCustomClickActionPacket packet) {
if (player.getConnectionInFlight() != null) { VelocityServerConnection serverConnection = player.getConnectionInFlightOrConnectedServer();
player.getConnectionInFlight().ensureConnected().write(packet.retain()); if (serverConnection != null) {
serverConnection.ensureConnected().write(packet.retain());
return true; return true;
} }
@@ -240,8 +242,8 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) { if (smc != null && serverConnection.getPhase().consideredComplete()) {
if (packet instanceof PluginMessagePacket) { if (packet instanceof ByteBufHolder bufHolder) {
((PluginMessagePacket) packet).retain(); bufHolder.retain();
} }
smc.write(packet); smc.write(packet);
} }
@@ -26,10 +26,13 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent; import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.PlayerClientLoadedWorldEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.ConnectionTypes;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
@@ -50,6 +53,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.RespawnPacket; import com.velocitypowered.proxy.protocol.packet.RespawnPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
@@ -241,6 +245,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return true; // will forward onto the server return true; // will forward onto the server
} }
@Override
public boolean handle(ServerboundPlayerLoadedPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
if (serverConnection == null) {
// No server connection yet, probably transitioning - shouldn't be possible with a vanilla client
return true;
}
if (!serverConnection.isClientLoaded()) {
serverConnection.setClientLoaded(true);
server.getEventManager().fireAndForget(new PlayerClientLoadedWorldEvent(player));
}
return false;
}
@Override @Override
public boolean handle(SessionPlayerCommandPacket packet) { public boolean handle(SessionPlayerCommandPacket packet) {
if (player.getCurrentServer().isEmpty()) { if (player.getCurrentServer().isEmpty()) {
@@ -50,9 +50,9 @@ import java.net.http.HttpResponse;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@@ -65,6 +65,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public class InitialLoginSessionHandler implements MinecraftSessionHandler { public class InitialLoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(InitialLoginSessionHandler.class); private static final Logger logger = LogManager.getLogger(InitialLoginSessionHandler.class);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final String MOJANG_HASJOINED_URL = private static final String MOJANG_HASJOINED_URL =
System.getProperty("mojang.sessionserver", System.getProperty("mojang.sessionserver",
"https://sessionserver.mojang.com/session/minecraft/hasJoined") "https://sessionserver.mojang.com/session/minecraft/hasJoined")
@@ -199,8 +200,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret()); byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
mcConnection.enableEncryption(decryptedSharedSecret);
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString(); String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
String url = String.format(MOJANG_HASJOINED_URL, String url = String.format(MOJANG_HASJOINED_URL,
urlFormParameterEscaper().escape(login.getUsername()), serverId); urlFormParameterEscaper().escape(login.getUsername()), serverId);
@@ -229,18 +234,6 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
return; return;
} }
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
try {
mcConnection.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption for connection", e);
// At this point, the connection is encrypted, but something's wrong on our side and
// we can't do anything about it.
mcConnection.close(true);
return;
}
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
final GameProfile profile = GENERAL_GSON.fromJson(response.body(), final GameProfile profile = GENERAL_GSON.fromJson(response.body(),
GameProfile.class); GameProfile.class);
@@ -280,7 +273,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
private EncryptionRequestPacket generateEncryptionRequest() { private EncryptionRequestPacket generateEncryptionRequest() {
byte[] verify = new byte[4]; byte[] verify = new byte[4];
ThreadLocalRandom.current().nextBytes(verify); SECURE_RANDOM.nextBytes(verify);
EncryptionRequestPacket request = new EncryptionRequestPacket(); EncryptionRequestPacket request = new EncryptionRequestPacket();
request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded()); request.setPublicKey(server.getServerKeyPair().getPublic().getEncoded());
@@ -60,9 +60,7 @@ final class CustomHandlerAdapter<F> {
UntargetedEventHandler buildUntargetedHandler(final Method method) UntargetedEventHandler buildUntargetedHandler(final Method method)
throws IllegalAccessException { throws IllegalAccessException {
final MethodHandle methodHandle = methodHandlesLookup.unreflect(method); final MethodHandle methodHandle = methodHandlesLookup.unreflect(method);
final MethodHandles.Lookup defineLookup = MethodHandles.privateLookupIn( final LambdaType<F> lambdaType = functionType.defineClassesWith(methodHandlesLookup);
method.getDeclaringClass(), methodHandlesLookup);
final LambdaType<F> lambdaType = functionType.defineClassesWith(defineLookup);
final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle); final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle);
final BiFunction<Object, Object, EventTask> handlerFunction = final BiFunction<Object, Object, EventTask> handlerFunction =
handlerBuilder.apply(invokeFunction); handlerBuilder.apply(invokeFunction);
@@ -243,7 +243,7 @@ public class VelocityEventManager implements EventManager {
} else { } else {
type = untargetedVoidHandlerType; type = untargetedVoidHandlerType;
} }
return LambdaFactory.create(type.defineClassesWith(lookup), methodHandle); return LambdaFactory.create(type.defineClassesWith(methodHandlesLookup), methodHandle);
} }
static final class MethodHandlerInfo { static final class MethodHandlerInfo {
@@ -46,10 +46,12 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -62,6 +64,7 @@ public class VelocityPluginManager implements PluginManager {
private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>(); private final Map<String, PluginContainer> pluginsById = new LinkedHashMap<>();
private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>(); private final Map<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
private final Set<PluginContainer> plugins = new LinkedHashSet<>();
private final VelocityServer server; private final VelocityServer server;
public VelocityPluginManager(VelocityServer server) { public VelocityPluginManager(VelocityServer server) {
@@ -74,7 +77,9 @@ public class VelocityPluginManager implements PluginManager {
* @param plugin the plugin to register * @param plugin the plugin to register
*/ */
public void registerPlugin(PluginContainer plugin) { public void registerPlugin(PluginContainer plugin) {
plugins.add(plugin);
pluginsById.put(plugin.getDescription().getId(), plugin); pluginsById.put(plugin.getDescription().getId(), plugin);
plugin.getDescription().getProvidedIds().forEach(id -> pluginsById.put(id, plugin));
Optional<?> instance = plugin.getInstance(); Optional<?> instance = plugin.getInstance();
instance.ifPresent(o -> pluginInstances.put(o, plugin)); instance.ifPresent(o -> pluginInstances.put(o, plugin));
} }
@@ -100,16 +105,34 @@ public class VelocityPluginManager implements PluginManager {
try { try {
PluginDescription candidate = loader.loadCandidate(path); PluginDescription candidate = loader.loadCandidate(path);
// If we found a duplicate candidate (with the same ID), don't load it. // A plugin claims its own ID plus every ID it provides. If any of those are already
PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent( // claimed by another candidate, don't load this one.
candidate.getId(), candidate); List<String> claimedIds = new ArrayList<>(candidate.getProvidedIds().size() + 1);
claimedIds.add(candidate.getId());
claimedIds.addAll(candidate.getProvidedIds());
if (maybeExistingCandidate != null) { PluginDescription conflict = null;
logger.error("Refusing to load plugin at path {} since we already " String conflictingId = null;
+ "loaded a plugin with the same ID {} from {}", for (String id : claimedIds) {
PluginDescription existing = foundCandidates.get(id);
if (existing != null) {
conflict = existing;
conflictingId = id;
break;
}
}
if (conflict != null) {
logger.error("Refusing to load plugin at path {} since ID {} was already "
+ "claimed by a plugin loaded from {}",
candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"), candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"),
candidate.getId(), conflictingId,
maybeExistingCandidate.getSource().map(Objects::toString).orElse("<UNKNOWN>")); conflict.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
continue;
}
for (String id : claimedIds) {
foundCandidates.put(id, candidate);
} }
} catch (Throwable e) { } catch (Throwable e) {
logger.error("Unable to load plugin {}", path, e); logger.error("Unable to load plugin {}", path, e);
@@ -122,8 +145,10 @@ public class VelocityPluginManager implements PluginManager {
return; return;
} }
// foundCandidates indexes each candidate under its ID and any provided IDs, so dedupe before
// sorting to avoid loading a plugin more than once.
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates( List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
new ArrayList<>(foundCandidates.values())); new ArrayList<>(new LinkedHashSet<>(foundCandidates.values())));
Map<String, PluginDescription> loadedCandidates = new HashMap<>(); Map<String, PluginDescription> loadedCandidates = new HashMap<>();
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>(); Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
@@ -144,6 +169,7 @@ public class VelocityPluginManager implements PluginManager {
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
pluginContainers.put(container, loader.createModule(container)); pluginContainers.put(container, loader.createModule(container));
loadedCandidates.put(realPlugin.getId(), realPlugin); loadedCandidates.put(realPlugin.getId(), realPlugin);
realPlugin.getProvidedIds().forEach(id -> loadedCandidates.putIfAbsent(id, realPlugin));
} catch (Throwable e) { } catch (Throwable e) {
logger.error("Can't create module for plugin {}", candidate.getId(), e); logger.error("Can't create module for plugin {}", candidate.getId(), e);
} }
@@ -201,7 +227,7 @@ public class VelocityPluginManager implements PluginManager {
@Override @Override
public Collection<PluginContainer> getPlugins() { public Collection<PluginContainer> getPlugins() {
return Collections.unmodifiableCollection(pluginsById.values()); return Collections.unmodifiableCollection(plugins);
} }
@Override @Override
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.meta.PluginDependency; import com.velocitypowered.api.plugin.meta.PluginDependency;
@@ -43,6 +44,7 @@ public class VelocityPluginDescription implements PluginDescription {
private final @Nullable String url; private final @Nullable String url;
private final List<String> authors; private final List<String> authors;
private final Map<String, PluginDependency> dependencies; private final Map<String, PluginDependency> dependencies;
private final Collection<String> providedIds;
private final Path source; private final Path source;
/** /**
@@ -55,11 +57,13 @@ public class VelocityPluginDescription implements PluginDescription {
* @param url the website for the plugin * @param url the website for the plugin
* @param authors the authors of this plugin * @param authors the authors of this plugin
* @param dependencies the dependencies for this plugin * @param dependencies the dependencies for this plugin
* @param providedIds the IDs this plugin provides for
* @param source the original source for the plugin * @param source the original source for the plugin
*/ */
public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version, public VelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source) { @Nullable List<String> authors, Collection<PluginDependency> dependencies,
@Nullable Collection<String> providedIds, Path source) {
this.id = checkNotNull(id, "id"); this.id = checkNotNull(id, "id");
this.name = Strings.emptyToNull(name); this.name = Strings.emptyToNull(name);
this.version = Strings.emptyToNull(version); this.version = Strings.emptyToNull(version);
@@ -67,6 +71,8 @@ public class VelocityPluginDescription implements PluginDescription {
this.url = Strings.emptyToNull(url); this.url = Strings.emptyToNull(url);
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors); this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId()); this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.getId());
this.providedIds =
providedIds == null ? ImmutableSet.of() : ImmutableSet.copyOf(providedIds);
this.source = source; this.source = source;
} }
@@ -110,6 +116,11 @@ public class VelocityPluginDescription implements PluginDescription {
return Optional.ofNullable(dependencies.get(id)); return Optional.ofNullable(dependencies.get(id));
} }
@Override
public Collection<String> getProvidedIds() {
return providedIds;
}
@Override @Override
public Optional<Path> getSource() { public Optional<Path> getSource() {
return Optional.ofNullable(source); return Optional.ofNullable(source);
@@ -125,6 +136,7 @@ public class VelocityPluginDescription implements PluginDescription {
+ ", url='" + url + '\'' + ", url='" + url + '\''
+ ", authors=" + authors + ", authors=" + authors
+ ", dependencies=" + dependencies + ", dependencies=" + dependencies
+ ", providedIds=" + providedIds
+ ", source=" + source + ", source=" + source
+ '}'; + '}';
} }
@@ -78,6 +78,14 @@ public class JavaPluginLoader implements PluginLoader {
} }
} }
for (String providedId : pd.getProvides()) {
if (!SerializedPluginDescription.ID_PATTERN.matcher(providedId).matches()) {
throw new InvalidPluginException(
"Provided ID '" + providedId + "' for plugin '" + pd.getId() + "' is invalid."
);
}
}
return createCandidateDescription(pd, source); return createCandidateDescription(pd, source);
} }
@@ -181,6 +189,7 @@ public class JavaPluginLoader implements PluginLoader {
description.getUrl(), description.getUrl(),
description.getAuthors(), description.getAuthors(),
dependencies, dependencies,
description.getProvides(),
source, source,
description.getMain() description.getMain()
); );
@@ -197,6 +206,7 @@ public class JavaPluginLoader implements PluginLoader {
description.getUrl().orElse(null), description.getUrl().orElse(null),
description.getAuthors(), description.getAuthors(),
description.getDependencies(), description.getDependencies(),
description.getProvidedIds(),
description.getSource().orElse(null), description.getSource().orElse(null),
mainClass mainClass
); );
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescription extends VelocityPluginDescription {
JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version, JavaVelocityPluginDescription(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source, @Nullable List<String> authors, Collection<PluginDependency> dependencies,
Class<?> mainClass) { @Nullable Collection<String> providedIds, Path source, Class<?> mainClass) {
super(id, name, version, description, url, authors, dependencies, source); super(id, name, version, description, url, authors, dependencies, providedIds, source);
this.mainClass = checkNotNull(mainClass); this.mainClass = checkNotNull(mainClass);
} }
@@ -32,9 +32,9 @@ class JavaVelocityPluginDescriptionCandidate extends VelocityPluginDescription {
JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version, JavaVelocityPluginDescriptionCandidate(String id, @Nullable String name, @Nullable String version,
@Nullable String description, @Nullable String url, @Nullable String description, @Nullable String url,
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source, @Nullable List<String> authors, Collection<PluginDependency> dependencies,
String mainClass) { @Nullable Collection<String> providedIds, Path source, String mainClass) {
super(id, name, version, description, url, authors, dependencies, source); super(id, name, version, description, url, authors, dependencies, providedIds, source);
this.mainClass = checkNotNull(mainClass); this.mainClass = checkNotNull(mainClass);
} }
@@ -17,7 +17,6 @@
package com.velocitypowered.proxy.plugin.util; package com.velocitypowered.proxy.plugin.util;
import com.google.common.collect.Maps;
import com.google.common.graph.Graph; import com.google.common.graph.Graph;
import com.google.common.graph.GraphBuilder; import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph; import com.google.common.graph.MutableGraph;
@@ -60,8 +59,19 @@ public class PluginDependencyUtils {
.allowsSelfLoops(false) .allowsSelfLoops(false)
.expectedNodeCount(sortedCandidates.size()) .expectedNodeCount(sortedCandidates.size())
.build(); .build();
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(sortedCandidates,
PluginDescription::getId); // Index candidates by their own ID and any IDs they provide, so a dependency can be satisfied
// by a plugin that provides that ID. Real IDs take precedence over provided IDs, and the first
// provider of a given ID wins; upstream loading rejects such conflicts before we get here.
Map<String, PluginDescription> candidateMap = new HashMap<>();
for (PluginDescription description : sortedCandidates) {
candidateMap.putIfAbsent(description.getId(), description);
}
for (PluginDescription description : sortedCandidates) {
for (String provided : description.getProvidedIds()) {
candidateMap.putIfAbsent(provided, description);
}
}
for (PluginDescription description : sortedCandidates) { for (PluginDescription description : sortedCandidates) {
graph.addNode(description); graph.addNode(description);
@@ -69,7 +79,8 @@ public class PluginDependencyUtils {
for (PluginDependency dependency : description.getDependencies()) { for (PluginDependency dependency : description.getDependencies()) {
PluginDescription in = candidateMap.get(dependency.getId()); PluginDescription in = candidateMap.get(dependency.getId());
if (in != null) { // Guard against self-loops: a plugin may name an ID it itself provides.
if (in != null && !in.equals(description)) {
graph.putEdge(description, in); graph.putEdge(description, in);
} }
} }
@@ -18,37 +18,7 @@
package com.velocitypowered.proxy.protocol; package com.velocitypowered.proxy.protocol;
import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.getLast;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12; import static com.velocitypowered.api.network.ProtocolVersion.*;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_12_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_14;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_15;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION;
import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.CLIENTBOUND; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.CLIENTBOUND;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.SERVERBOUND; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction.SERVERBOUND;
@@ -88,6 +58,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundPlayerLoadedPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -95,6 +66,7 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.UpdateTeamsPacket;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
@@ -344,6 +316,12 @@ public enum StateRegistry {
map(0x13, MINECRAFT_1_21_2, false), map(0x13, MINECRAFT_1_21_2, false),
map(0x14, MINECRAFT_1_21_6, false), map(0x14, MINECRAFT_1_21_6, false),
map(0x15, MINECRAFT_26_1, false)); map(0x15, MINECRAFT_26_1, false));
serverbound.register(
ServerboundPlayerLoadedPacket.class,
() -> ServerboundPlayerLoadedPacket.INSTANCE,
map(0x2A, MINECRAFT_1_21_4, false),
map(0x2B, MINECRAFT_1_21_6, false),
map(0x2C, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
PluginMessagePacket.class, PluginMessagePacket.class,
PluginMessagePacket::new, PluginMessagePacket::new,
@@ -836,6 +814,25 @@ public enum StateRegistry {
map(0x82, MINECRAFT_1_21_2, false), map(0x82, MINECRAFT_1_21_2, false),
map(0x87, MINECRAFT_1_21_9, false), map(0x87, MINECRAFT_1_21_9, false),
map(0x89, MINECRAFT_26_1, false)); map(0x89, MINECRAFT_26_1, false));
clientbound.register(UpdateTeamsPacket.class, UpdateTeamsPacket::new,
map(0x41, MINECRAFT_1_9, true),
map(0x43, MINECRAFT_1_12, true),
map(0x44, MINECRAFT_1_12_1, true),
map(0x47, MINECRAFT_1_13, true),
map(0x4B, MINECRAFT_1_14, true),
map(0x4C, MINECRAFT_1_15, true),
map(0x55, MINECRAFT_1_17, true),
map(0x58, MINECRAFT_1_19_1, true),
map(0x56, MINECRAFT_1_19_3, true),
map(0x5A, MINECRAFT_1_19_4, true),
map(0x5C, MINECRAFT_1_20_2, true),
map(0x5E, MINECRAFT_1_20_3, true),
map(0x60, MINECRAFT_1_20_5, true),
map(0x67, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_9, false),
map(0x63, MINECRAFT_1_21_11, false),
map(0x6D, MINECRAFT_26_1, false),
map(0x6D, MINECRAFT_26_2, false));
} }
}, },
LOGIN { LOGIN {
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class ServerboundPlayerLoadedPacket implements MinecraftPacket {
public static final ServerboundPlayerLoadedPacket INSTANCE = new ServerboundPlayerLoadedPacket();
private ServerboundPlayerLoadedPacket() {
}
@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
}
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 0;
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}
@@ -31,8 +31,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public class TabCompleteRequestPacket implements MinecraftPacket { public class TabCompleteRequestPacket implements MinecraftPacket {
private static final int VANILLA_MAX_TAB_COMPLETE_LEN = 2048;
private @Nullable String command; private @Nullable String command;
private int transactionId; private int transactionId;
private boolean assumeCommand; private boolean assumeCommand;
@@ -97,9 +95,11 @@ public class TabCompleteRequestPacket implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.noLessThan(MINECRAFT_1_13)) { if (version.noLessThan(MINECRAFT_1_13)) {
this.transactionId = ProtocolUtils.readVarInt(buf); this.transactionId = ProtocolUtils.readVarInt(buf);
this.command = ProtocolUtils.readString(buf, VANILLA_MAX_TAB_COMPLETE_LEN);
// 1.13 only supports a max length of 256: https://bugs.mojang.com/browse/MC/issues/MC-132663
this.command = ProtocolUtils.readString(buf, version.equals(MINECRAFT_1_13) ? 256 : 32500);
} else { } else {
this.command = ProtocolUtils.readString(buf, VANILLA_MAX_TAB_COMPLETE_LEN); this.command = ProtocolUtils.readString(buf, 32767);
if (version.noLessThan(MINECRAFT_1_9)) { if (version.noLessThan(MINECRAFT_1_9)) {
this.assumeCommand = buf.readBoolean(); this.assumeCommand = buf.readBoolean();
} }
@@ -0,0 +1,197 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.text.Component;
import java.util.List;
public class UpdateTeamsPacket implements MinecraftPacket {
protected String name;
protected Mode mode;
protected Component displayName;
protected Component prefix;
protected Component suffix;
protected NameTagVisibility nameTagVisibility;
protected CollisionRule collisionRule;
protected int color;
protected byte friendlyFlags;
protected List<String> players;
public UpdateTeamsPacket(String name, Mode mode, Component displayName, Component prefix, Component suffix, NameTagVisibility nameTagVisibility, CollisionRule collisionRule, int color, byte friendlyFlags, List<String> players) {
this.name = name;
this.mode = mode;
this.displayName = displayName;
this.prefix = prefix;
this.suffix = suffix;
this.nameTagVisibility = nameTagVisibility;
this.collisionRule = collisionRule;
this.color = color;
this.friendlyFlags = friendlyFlags;
this.players = players;
}
public UpdateTeamsPacket() {
}
@Override
public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException("Packet is not implemented");
}
@Override
public boolean handle(MinecraftSessionHandler minecraftSessionHandler) {
return false;
}
@Override
public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException("Packet is not implemented");
}
public enum Mode {
CREATE,
REMOVE,
UPDATE,
ADD_PLAYER,
REMOVE_PLAYER,
}
public enum NameTagVisibility {
ALWAYS("always"),
NEVER("never"),
HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"),
HIDE_FOR_OWN_TEAM("hideForOwnTeam");
private final String value;
NameTagVisibility(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
public enum CollisionRule {
ALWAYS("always"),
NEVER("never"),
PUSH_OTHER_TEAMS("pushOtherTeams"),
PUSH_OWN_TEAM("pushOwnTeam");
private final String value;
CollisionRule(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
public String getName() {
return name;
}
public Mode getMode() {
return mode;
}
public Component getDisplayName() {
return displayName;
}
public Component getPrefix() {
return prefix;
}
public Component getSuffix() {
return suffix;
}
public NameTagVisibility getNameTagVisibility() {
return nameTagVisibility;
}
public CollisionRule getCollisionRule() {
return collisionRule;
}
public int getColor() {
return color;
}
public byte getFriendlyFlags() {
return friendlyFlags;
}
public List<String> getPlayers() {
return players;
}
public void setName(String name) {
this.name = name;
}
public void setMode(Mode mode) {
this.mode = mode;
}
public void setDisplayName(Component displayName) {
this.displayName = displayName;
}
public void setPrefix(Component prefix) {
this.prefix = prefix;
}
public void setSuffix(Component suffix) {
this.suffix = suffix;
}
public void setNameTagVisibility(NameTagVisibility nameTagVisibility) {
this.nameTagVisibility = nameTagVisibility;
}
public void setCollisionRule(CollisionRule collisionRule) {
this.collisionRule = collisionRule;
}
public void setColor(int color) {
this.color = color;
}
public void setFriendlyFlags(byte friendlyFlags) {
this.friendlyFlags = friendlyFlags;
}
public void setPlayers(List<String> players) {
this.players = players;
}
}
@@ -116,6 +116,8 @@ public class KeyedPlayerChatPacket implements MinecraftPacket {
ProtocolUtils.readByteArray(buf)); ProtocolUtils.readByteArray(buf));
} }
} }
unsigned = true;
} }
@Override @Override
@@ -132,6 +132,7 @@ public class KeyedPlayerCommandPacket implements MinecraftPacket {
unsigned = true; unsigned = true;
} }
unsigned = true;
} }
@Override @Override
@@ -69,6 +69,7 @@ public class SessionPlayerChatPacket implements MinecraftPacket {
this.salt = buf.readLong(); this.salt = buf.readLong();
this.signed = buf.readBoolean(); this.signed = buf.readBoolean();
if (this.signed) { if (this.signed) {
this.signed = false;
this.signature = readMessageSignature(buf); this.signature = readMessageSignature(buf);
} else { } else {
this.signature = new byte[0]; this.signature = new byte[0];
@@ -47,6 +47,8 @@ public class SessionPlayerCommandPacket implements MinecraftPacket {
this.salt = buf.readLong(); this.salt = buf.readLong();
this.argumentSignatures = new ArgumentSignatures(buf); this.argumentSignatures = new ArgumentSignatures(buf);
this.lastSeenMessages = new LastSeenMessages(buf, protocolVersion); this.lastSeenMessages = new LastSeenMessages(buf, protocolVersion);
this.argumentSignatures = new ArgumentSignatures();
} }
@Override @Override
@@ -28,7 +28,7 @@ public class TitleActionbarPacket extends GenericTitlePacket {
private ComponentHolder component; private ComponentHolder component;
public TitleActionbarPacket() { public TitleActionbarPacket() {
setAction(ActionType.SET_TITLE); setAction(ActionType.SET_ACTION_BAR);
} }
@Override @Override
@@ -31,6 +31,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@@ -95,7 +96,7 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList {
entry.setLatencyInternal(item.getLatency()); entry.setLatencyInternal(item.getLatency());
} }
} else { } else {
UUID uuid = UUID.randomUUID(); // Use a fake uuid to preserve function of custom entries UUID uuid = generateInsecureRandomUuid(); // Use a fake uuid to preserve function of custom entries
nameMapping.put(item.getName(), uuid); nameMapping.put(item.getName(), uuid);
entries.put(uuid, (KeyedVelocityTabListEntry) TabListEntry.builder() entries.put(uuid, (KeyedVelocityTabListEntry) TabListEntry.builder()
.tabList(this) .tabList(this)
@@ -153,4 +154,17 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList {
int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) { int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) {
return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode); return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode);
} }
/**
* Generates a random UUID v4 using {@link ThreadLocalRandom}. The result is a structurally valid
* UUID v4 but is not cryptographically secure
*
* @return a new random {@link UUID}
*/
private static UUID generateInsecureRandomUuid() {
ThreadLocalRandom random = ThreadLocalRandom.current();
long msb = (random.nextLong() & 0xffffffffffff0fffL) | 0x0000000000004000L; // version 4
long lsb = (random.nextLong() & 0x3fffffffffffffffL) | 0x8000000000000000L; // IETF variant
return new UUID(msb, lsb);
}
} }
@@ -50,6 +50,7 @@ velocity.command.glist-view-all=To view all players on servers, use /glist all.
velocity.command.reload-success=Velocity configuration successfully reloaded. velocity.command.reload-success=Velocity configuration successfully reloaded.
velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details. velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details.
velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3. velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3.
velocity.command.version-offer-copy-version=Click to copy version to clipboard
velocity.command.no-plugins=There are no plugins currently installed. velocity.command.no-plugins=There are no plugins currently installed.
velocity.command.plugins-list=Plugins: <arg:0> velocity.command.plugins-list=Plugins: <arg:0>
velocity.command.plugin-tooltip-website=Website: <arg:0> velocity.command.plugin-tooltip-website=Website: <arg:0>
@@ -0,0 +1,119 @@
/*
* Copyright (C) 2018-2026 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.connection.client;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCountUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class ClientConfigSessionHandlerTest {
private VelocityServer server;
private ConnectedPlayer player;
private ClientConfigSessionHandler handler;
@BeforeEach
void setUp() {
server = mock(VelocityServer.class);
player = mock(ConnectedPlayer.class);
handler = new ClientConfigSessionHandler(server, player);
}
@AfterEach
void tearDown() {
// nothing to clean up; each test manages its own ByteBufs
}
private ServerboundCustomClickActionPacket makePacket() {
ByteBuf frame = Unpooled.buffer().writeByte(0);
ServerboundCustomClickActionPacket pkt = new ServerboundCustomClickActionPacket();
pkt.replace(frame.readRetainedSlice(frame.readableBytes()));
return pkt;
}
@Test
void handleForwardsToInFlightServer() {
VelocityServerConnection inFlight = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(inFlight);
when(inFlight.ensureConnected()).thenReturn(backend);
ServerboundCustomClickActionPacket pkt = makePacket();
assertTrue(handler.handle(pkt));
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
@Test
void handleForwardsToConnectedServerWhenInFlightIsNull() {
VelocityServerConnection connected = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(connected);
when(connected.ensureConnected()).thenReturn(backend);
ServerboundCustomClickActionPacket pkt = makePacket();
assertTrue(handler.handle(pkt));
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
@Test
void handleReturnsFalseWhenNoServer() {
when(player.getConnectionInFlightOrConnectedServer()).thenReturn(null);
ServerboundCustomClickActionPacket pkt = makePacket();
assertFalse(handler.handle(pkt));
ReferenceCountUtil.release(pkt);
}
@Test
void handleGenericRetainsAndForwards() {
VelocityServerConnection connected = mock(VelocityServerConnection.class);
MinecraftConnection backend = mock(MinecraftConnection.class);
BackendConnectionPhase phase = mock(BackendConnectionPhase.class);
when(player.getConnectedServer()).thenReturn(connected);
when(connected.getConnection()).thenReturn(backend);
when(connected.getPhase()).thenReturn(phase);
when(phase.consideredComplete()).thenReturn(true);
ServerboundCustomClickActionPacket pkt = makePacket();
int refBefore = pkt.refCnt();
handler.handleGeneric(pkt);
// retain() was called (+1) before write
assertEquals(refBefore + 1, pkt.refCnt());
verify(backend).write(pkt);
ReferenceCountUtil.release(pkt);
}
}
@@ -44,6 +44,15 @@ class PluginDependencyUtilsTest {
private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval", private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval",
new PluginDependency("circle", "", false)); new PluginDependency("circle", "", false));
// "provider" is loaded from a real ID but provides the virtual ID "some-api"; "consumer" and
// "zdependent" depend on it only through that provided ID / a chain that reaches it.
private static final PluginDescription PROVIDES_API = providingDescription("provider",
ImmutableList.of("some-api"));
private static final PluginDescription DEPENDS_ON_PROVIDED = providingDescription("consumer",
ImmutableList.of(), new PluginDependency("some-api", null, false));
private static final PluginDescription DEPENDS_ON_CONSUMER = testDescription("zdependent",
new PluginDependency("consumer", null, false));
@Test @Test
void sortCandidatesTrivial() throws Exception { void sortCandidatesTrivial() throws Exception {
List<PluginDescription> descriptionList = new ArrayList<>(); List<PluginDescription> descriptionList = new ArrayList<>();
@@ -96,10 +105,31 @@ class PluginDependencyUtilsTest {
assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs)); assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs));
} }
@Test
void sortCandidatesResolvesProvidedDependency() throws Exception {
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_PROVIDED, PROVIDES_API);
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED);
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
}
@Test
void sortCandidatesResolvesTransitiveProvidedDependency() throws Exception {
List<PluginDescription> plugins = ImmutableList.of(DEPENDS_ON_CONSUMER, DEPENDS_ON_PROVIDED,
PROVIDES_API);
List<PluginDescription> expected = ImmutableList.of(PROVIDES_API, DEPENDS_ON_PROVIDED,
DEPENDS_ON_CONSUMER);
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
}
private static PluginDescription testDescription(String id, PluginDependency... dependencies) { private static PluginDescription testDescription(String id, PluginDependency... dependencies) {
return providingDescription(id, ImmutableList.of(), dependencies);
}
private static PluginDescription providingDescription(String id, List<String> provides,
PluginDependency... dependencies) {
return new VelocityPluginDescription( return new VelocityPluginDescription(
id, "tuxed", "0.1", null, null, ImmutableList.of(), id, "tuxed", "0.1", null, null, ImmutableList.of(),
ImmutableList.copyOf(dependencies), null ImmutableList.copyOf(dependencies), provides, null
); );
} }
} }
+9
View File
@@ -0,0 +1,9 @@
build:
- "./gradlew build -x check -x javadoc --no-daemon"
artifacts:
"/jars/Velocity.jar": "proxy/build/libs/velocity-proxy-3.5.0-SNAPSHOT-all.jar"
release:
- "mvn deploy:deploy-file -DgroupId=de.steamwar -DartifactId=velocity -Dversion=RELEASE -Dpackaging=jar -Dfile=/jars/Velocity.jar -Durl=file:///var/www/maven/"