Compare commits
58
Commits
4b8ceb5264
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a39ebb058 | ||
|
|
57e9cd7f24 | ||
|
|
4772ca3022 | ||
|
|
b9463125d1 | ||
|
|
f918d0d649 | ||
|
|
e6fbcc9196 | ||
|
|
14a69904f9 | ||
|
|
71c50a75eb | ||
|
|
00759e5279 | ||
|
|
06ade4775e | ||
|
|
2676520c6a | ||
|
|
e11584ba35 | ||
|
|
a08972749b | ||
|
|
c6e9ca989e | ||
|
|
d30f1d9a74 | ||
|
|
b45716deff | ||
|
|
e653647962 | ||
|
|
5aab0d1427 | ||
|
|
1cd8d51d02 | ||
|
|
9eb338bd1c | ||
|
|
60380211f8 | ||
|
|
3b239daf4c | ||
|
|
90f89053a7 | ||
|
|
67e16a308e
|
||
|
|
4bd30bac33 | ||
|
|
28c9f5a356 | ||
|
|
da7427fb51 | ||
|
|
b8d1f16b07 | ||
|
|
729a050662 | ||
|
|
c690b4abfc | ||
|
|
5cc6b0b7e5 | ||
|
|
757711a5bc | ||
|
|
95694cba28 | ||
|
|
7cb01b85f0 | ||
|
|
0942e162c0 | ||
|
|
c1cd71a4bc | ||
|
|
843a47e2a3 | ||
|
|
4498f1e03d | ||
|
|
1abb3ba56a | ||
|
|
a5680fc223 | ||
|
|
ffca982a9d | ||
|
|
2b5d964ab0 | ||
|
|
f11cc86e69 | ||
|
|
06eb052ab5 | ||
|
|
97b386d86f | ||
|
|
81a5817a82 | ||
|
|
1edab1411d | ||
|
|
a7581821fb | ||
|
|
d7ad0522e9 | ||
|
|
0cbe10e951 | ||
|
|
bcf1bba3a1 | ||
|
|
3b892899db | ||
|
|
1b64a70a00 | ||
|
|
3b142f3099 | ||
|
|
25fbd833cd | ||
|
|
b72cf26802 | ||
|
|
7d68208e11 | ||
|
|
38ff21afd7 |
@@ -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)
|
||||||
@@ -59,17 +59,15 @@ tasks {
|
|||||||
|
|
||||||
val o = options as StandardJavadocDocletOptions
|
val o = options as StandardJavadocDocletOptions
|
||||||
o.encoding = "UTF-8"
|
o.encoding = "UTF-8"
|
||||||
o.source = "21"
|
o.source = "25"
|
||||||
|
|
||||||
o.use()
|
o.use()
|
||||||
o.links(
|
o.links(
|
||||||
"https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/",
|
"https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/",
|
||||||
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
|
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
|
||||||
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
|
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
|
||||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
"https://docs.oracle.com/en/java/javase/25/docs/api/",
|
||||||
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/",
|
"https://jd.papermc.io/adventure/${libs.adventure.bom.get().version}/",
|
||||||
"https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/",
|
|
||||||
"https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/",
|
|
||||||
"https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
|
"https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides events for handling command execution.
|
||||||
|
*/
|
||||||
|
package com.velocitypowered.api.event.command;
|
||||||
+45
@@ -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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides events for handling the player configuration phase.
|
||||||
|
*/
|
||||||
|
package com.velocitypowered.api.event.player.configuration;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides events for handling registration of servers on the proxy.
|
||||||
|
*/
|
||||||
|
package com.velocitypowered.api.event.proxy.server;
|
||||||
@@ -95,7 +95,8 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
|
|||||||
MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"),
|
MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"),
|
||||||
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
|
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
|
||||||
MINECRAFT_1_21_11(774, "1.21.11"),
|
MINECRAFT_1_21_11(774, "1.21.11"),
|
||||||
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2");
|
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2"),
|
||||||
|
MINECRAFT_26_2(776, "26.2");
|
||||||
|
|
||||||
private static final int SNAPSHOT_BIT = 30;
|
private static final int SNAPSHOT_BIT = 30;
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ public interface Player extends
|
|||||||
*
|
*
|
||||||
* @param reason component with the reason
|
* @param reason component with the reason
|
||||||
*/
|
*/
|
||||||
void disconnect(Component reason);
|
void disconnect(@NotNull Component reason);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends chat input onto the players current server as if they typed it into the client chat box.
|
* Sends chat input onto the players current server as if they typed it into the client chat box.
|
||||||
|
|||||||
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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
@@ -12,7 +12,7 @@ subprojects {
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion.set(JavaLanguageVersion.of(21))
|
languageVersion.set(JavaLanguageVersion.of(25))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
group=com.velocitypowered
|
group=com.velocitypowered
|
||||||
version=3.5.0-SNAPSHOT
|
version=4.1.0-SNAPSHOT
|
||||||
|
|||||||
+23
-24
@@ -2,25 +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.25.3"
|
log4j = "2.26.1"
|
||||||
netty = "4.2.10.Final"
|
netty = "4.2.16.Final"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
fill = "io.papermc.fill.gradle:1.0.10"
|
fill = "io.papermc.fill.gradle:1.0.12"
|
||||||
shadow = "com.gradleup.shadow:9.3.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:4.26.1"
|
adventure-bom = "net.kyori:adventure-bom:5.2.0"
|
||||||
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.26.1"
|
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0"
|
||||||
adventure-facet = "net.kyori:adventure-platform-facet:4.4.1"
|
asm = "org.ow2.asm:asm:9.10.1"
|
||||||
asm = "org.ow2.asm:asm:9.9.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" }
|
||||||
@@ -30,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.15"
|
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:5.14.2"
|
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.5.0-jre"
|
guava = "com.google.guava:guava:33.6.0-jre"
|
||||||
gson = "com.google.code.gson:gson:2.13.2"
|
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.21.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" }
|
||||||
@@ -55,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]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
|||||||
+26
-5
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+78
@@ -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
-61
@@ -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 }
|
||||||
@@ -159,7 +113,6 @@ dependencies {
|
|||||||
implementation(libs.fastutil)
|
implementation(libs.fastutil)
|
||||||
implementation(platform(libs.adventure.bom))
|
implementation(platform(libs.adventure.bom))
|
||||||
implementation(libs.adventure.text.serializer.json.legacy.impl)
|
implementation(libs.adventure.text.serializer.json.legacy.impl)
|
||||||
implementation(libs.adventure.facet)
|
|
||||||
implementation(libs.completablefutures)
|
implementation(libs.completablefutures)
|
||||||
implementation(libs.nightconfig)
|
implementation(libs.nightconfig)
|
||||||
implementation(libs.bstats)
|
implementation(libs.bstats)
|
||||||
|
|||||||
@@ -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("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -165,6 +165,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
|
|
||||||
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
|
||||||
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
|
||||||
|
private final Object sessionIdLock = new Object();
|
||||||
|
private volatile @Nullable UUID sessionId;
|
||||||
private final VelocityConsole console;
|
private final VelocityConsole console;
|
||||||
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
|
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
|
||||||
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
|
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
|
||||||
@@ -219,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;
|
||||||
@@ -743,6 +746,36 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
|||||||
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
|
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
|
||||||
connectionsByUuid.remove(connection.getUniqueId(), connection);
|
connectionsByUuid.remove(connection.getUniqueId(), connection);
|
||||||
connection.disconnected();
|
connection.disconnected();
|
||||||
|
|
||||||
|
if (this.sessionId != null && connectionsByUuid.isEmpty()) {
|
||||||
|
synchronized (this.sessionIdLock) {
|
||||||
|
if (connectionsByUuid.isEmpty()) {
|
||||||
|
this.sessionId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the metrics session ID for this proxy, generating one if none is currently active. The
|
||||||
|
* ID is shared by every player connected during a populated period and is regenerated once the
|
||||||
|
* proxy empties.
|
||||||
|
*
|
||||||
|
* @return the current session ID
|
||||||
|
*/
|
||||||
|
public UUID getSessionId() {
|
||||||
|
UUID uuid = this.sessionId;
|
||||||
|
if (uuid != null) {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
synchronized (this.sessionIdLock) {
|
||||||
|
uuid = this.sessionId;
|
||||||
|
if (uuid == null) {
|
||||||
|
uuid = UUID.randomUUID();
|
||||||
|
this.sessionId = uuid;
|
||||||
|
}
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import com.velocitypowered.proxy.config.migration.ForwardingMigration;
|
|||||||
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
|
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
|
||||||
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
|
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
|
||||||
import com.velocitypowered.proxy.config.migration.MotdMigration;
|
import com.velocitypowered.proxy.config.migration.MotdMigration;
|
||||||
|
import com.velocitypowered.proxy.config.migration.PacketLimiterMigration;
|
||||||
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
|
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
|
||||||
import com.velocitypowered.proxy.util.AddressUtil;
|
import com.velocitypowered.proxy.util.AddressUtil;
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
@@ -511,7 +512,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
new KeyAuthenticationMigration(),
|
new KeyAuthenticationMigration(),
|
||||||
new MotdMigration(),
|
new MotdMigration(),
|
||||||
new MiniMessageTranslationsMigration(),
|
new MiniMessageTranslationsMigration(),
|
||||||
new TransferIntegrationMigration()
|
new TransferIntegrationMigration(),
|
||||||
|
new PacketLimiterMigration()
|
||||||
};
|
};
|
||||||
|
|
||||||
for (final ConfigurationMigration migration : migrations) {
|
for (final ConfigurationMigration migration : migrations) {
|
||||||
@@ -1007,9 +1009,10 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
* @param interval the interval in seconds to measure packets over
|
* @param interval the interval in seconds to measure packets over
|
||||||
* @param pps the maximum number of packets per second allowed
|
* @param pps the maximum number of packets per second allowed
|
||||||
* @param bytes the maximum number of bytes per second allowed
|
* @param bytes the maximum number of bytes per second allowed
|
||||||
|
* @param bytesAfterDecompression the maximum number of decompressed bytes per second allowed
|
||||||
*/
|
*/
|
||||||
public record PacketLimiterConfig(int interval, int pps, int bytes) {
|
public record PacketLimiterConfig(int interval, int pps, int bytes, int bytesAfterDecompression) {
|
||||||
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1);
|
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, -1, -1, 5242880);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns a PacketLimiterConfig from a config section, or the default if the section is null.
|
* returns a PacketLimiterConfig from a config section, or the default if the section is null.
|
||||||
@@ -1022,7 +1025,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
return new PacketLimiterConfig(
|
return new PacketLimiterConfig(
|
||||||
config.getIntOrElse("interval", DEFAULT.interval()),
|
config.getIntOrElse("interval", DEFAULT.interval()),
|
||||||
config.getIntOrElse("packets-per-second", DEFAULT.pps()),
|
config.getIntOrElse("packets-per-second", DEFAULT.pps()),
|
||||||
config.getIntOrElse("bytes-per-second", DEFAULT.bytes())
|
config.getIntOrElse("bytes-per-second", DEFAULT.bytes()),
|
||||||
|
config.getIntOrElse("decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression())
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return DEFAULT;
|
return DEFAULT;
|
||||||
|
|||||||
+2
-1
@@ -29,7 +29,8 @@ public sealed interface ConfigurationMigration
|
|||||||
KeyAuthenticationMigration,
|
KeyAuthenticationMigration,
|
||||||
MotdMigration,
|
MotdMigration,
|
||||||
MiniMessageTranslationsMigration,
|
MiniMessageTranslationsMigration,
|
||||||
TransferIntegrationMigration {
|
TransferIntegrationMigration,
|
||||||
|
PacketLimiterMigration {
|
||||||
boolean shouldMigrate(CommentedFileConfig config);
|
boolean shouldMigrate(CommentedFileConfig config);
|
||||||
|
|
||||||
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
|
void migrate(CommentedFileConfig config, Logger logger) throws IOException;
|
||||||
|
|||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 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.config.migration;
|
||||||
|
|
||||||
|
import static com.velocitypowered.proxy.config.VelocityConfiguration.PacketLimiterConfig.DEFAULT;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration migration for the new [packet-limiter] section.
|
||||||
|
* Config version 2.7 may contain this section with only the `interval`, `packets-per-second`
|
||||||
|
* and `bytes-per-second` attributes. Config version 2.8 enforces these exist, adds the new
|
||||||
|
* `decompressed-bytes-per-second` attribute, adjusts the new default, and adds comments.
|
||||||
|
*/
|
||||||
|
public final class PacketLimiterMigration implements ConfigurationMigration {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldMigrate(CommentedFileConfig config) {
|
||||||
|
return configVersion(config) < 2.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(CommentedFileConfig config, Logger logger) {
|
||||||
|
config.set("packet-limiter.interval", DEFAULT.interval());
|
||||||
|
config.set("packet-limiter.packets-per-second", DEFAULT.pps());
|
||||||
|
config.set("packet-limiter.bytes-per-second", DEFAULT.bytes());
|
||||||
|
config.set("packet-limiter.decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression());
|
||||||
|
|
||||||
|
config.setComment("packet-limiter.interval", """
|
||||||
|
Size of the moving time window in seconds used to calculate average rates.
|
||||||
|
A larger window tolerates short bursts while still enforcing the configured limits over time.""");
|
||||||
|
|
||||||
|
config.setComment("packet-limiter.packets-per-second", """
|
||||||
|
Maximum average number of packets per second a client may send. -1 disables this check.""");
|
||||||
|
|
||||||
|
config.setComment("packet-limiter.bytes-per-second", """
|
||||||
|
Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.""");
|
||||||
|
|
||||||
|
config.setComment("packet-limiter.decompressed-bytes-per-second", """
|
||||||
|
Maximum average number of decompressed bytes per second a client may send.
|
||||||
|
Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
|
||||||
|
-1 disables this check.""");
|
||||||
|
|
||||||
|
config.set("config-version", "2.8");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,9 @@ import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
|
|||||||
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
|
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
|
import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
|
||||||
import com.velocitypowered.proxy.network.Connections;
|
import com.velocitypowered.proxy.network.Connections;
|
||||||
|
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
|
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
|
||||||
@@ -571,6 +573,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
||||||
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
|
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
|
||||||
|
|
||||||
|
var packetLimiterConfig = server.getConfiguration().getPacketLimiterConfig();
|
||||||
|
if (minecraftDecoder.getDirection() == ProtocolUtils.Direction.SERVERBOUND
|
||||||
|
&& packetLimiterConfig.interval() > 0
|
||||||
|
&& packetLimiterConfig.bytesAfterDecompression() > 0) {
|
||||||
|
decoder.setPacketLimiter(new SimpleBytesPerSecondLimiter(
|
||||||
|
-1, packetLimiterConfig.bytesAfterDecompression(), packetLimiterConfig.interval()));
|
||||||
|
}
|
||||||
|
|
||||||
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
|
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -296,6 +296,19 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register and unregister packets are simply forwarded to the client as-is.
|
||||||
|
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PluginMessageUtil.isMcBrand(packet)) {
|
||||||
|
PluginMessagePacket rewritten = PluginMessageUtil
|
||||||
|
.rewriteMinecraftBrand(packet,
|
||||||
|
server.getVersion(), playerConnection.getProtocolVersion());
|
||||||
|
playerConnection.write(rewritten);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
|
if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
|
||||||
// Handled.
|
// Handled.
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+1
-1
@@ -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))
|
||||||
|
|||||||
+4
-1
@@ -89,7 +89,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(JoinGamePacket packet) {
|
public boolean handle(JoinGamePacket packet) {
|
||||||
MinecraftConnection smc = serverConn.ensureConnected();
|
final MinecraftConnection smc = serverConn.ensureConnected();
|
||||||
final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null);
|
final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null);
|
||||||
final ConnectedPlayer player = serverConn.getPlayer();
|
final ConnectedPlayer player = serverConn.getPlayer();
|
||||||
final VelocityServerConnection existingConnection = player.getConnectedServer();
|
final VelocityServerConnection existingConnection = player.getConnectedServer();
|
||||||
@@ -106,6 +106,9 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
|||||||
// Reset Tablist header and footer to prevent desync
|
// Reset Tablist header and footer to prevent desync
|
||||||
player.clearPlayerListHeaderAndFooter();
|
player.clearPlayerListHeaderAndFooter();
|
||||||
|
|
||||||
|
// Override online mode
|
||||||
|
packet.setOnlineMode(player.isOnlineMode());
|
||||||
|
|
||||||
// The goods are in hand! We got JoinGame. Let's transition completely to the new state.
|
// The goods are in hand! We got JoinGame. Let's transition completely to the new state.
|
||||||
smc.setAutoReading(false);
|
smc.setAutoReading(false);
|
||||||
server.getEventManager()
|
server.getEventManager()
|
||||||
|
|||||||
+10
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -236,6 +236,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
|||||||
success.setUsername(player.getUsername());
|
success.setUsername(player.getUsername());
|
||||||
success.setProperties(player.getGameProfileProperties());
|
success.setProperties(player.getGameProfileProperties());
|
||||||
success.setUuid(player.getUniqueId());
|
success.setUuid(player.getUniqueId());
|
||||||
|
if (inbound.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
|
||||||
|
success.setSessionId(server.getSessionId());
|
||||||
|
}
|
||||||
mcConnection.write(success);
|
mcConnection.write(success);
|
||||||
|
|
||||||
loginState = State.SUCCESS_SENT;
|
loginState = State.SUCCESS_SENT;
|
||||||
|
|||||||
+7
-5
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
+84
-4
@@ -26,6 +26,7 @@ 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;
|
||||||
@@ -52,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;
|
||||||
@@ -88,6 +90,8 @@ import java.util.Queue;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.ComponentLike;
|
import net.kyori.adventure.text.ComponentLike;
|
||||||
@@ -104,12 +108,23 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
private static final boolean BACKPRESSURE_LOG =
|
private static final boolean BACKPRESSURE_LOG =
|
||||||
Boolean.getBoolean("velocity.log-server-backpressure");
|
Boolean.getBoolean("velocity.log-server-backpressure");
|
||||||
|
|
||||||
|
// Caps the per-connection queue used while the FML/login phases are not yet "complete". Without
|
||||||
|
// these caps, a client that never completes its handshake phase can spam plugin messages (each up
|
||||||
|
// to ~32 KiB serverbound) and grow the queue without bound.
|
||||||
|
private static final long MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES =
|
||||||
|
Long.getLong("velocity.max-queued-login-plugin-message-bytes", 4L * 1024 * 1024);
|
||||||
|
private static final int MAX_QUEUED_LOGIN_PLUGIN_MESSAGES =
|
||||||
|
Integer.getInteger("velocity.max-queued-login-plugin-messages", 1024);
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
|
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
|
||||||
|
|
||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private boolean spawned = false;
|
private boolean spawned = false;
|
||||||
private final List<UUID> serverBossBars = new ArrayList<>();
|
private final List<UUID> serverBossBars = new ArrayList<>();
|
||||||
private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>();
|
private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>();
|
||||||
|
private final AtomicLong loginPluginMessagesBytes = new AtomicLong();
|
||||||
|
private final AtomicInteger loginPluginMessagesCount = new AtomicInteger();
|
||||||
|
private volatile boolean loginPluginMessagesOverflowed;
|
||||||
private final VelocityServer server;
|
private final VelocityServer server;
|
||||||
private @Nullable TabCompleteRequestPacket outstandingTabComplete;
|
private @Nullable TabCompleteRequestPacket outstandingTabComplete;
|
||||||
private final ChatHandler<? extends MinecraftPacket> chatHandler;
|
private final ChatHandler<? extends MinecraftPacket> chatHandler;
|
||||||
@@ -178,9 +193,38 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
@Override
|
@Override
|
||||||
public void deactivated() {
|
public void deactivated() {
|
||||||
player.discardChatQueue();
|
player.discardChatQueue();
|
||||||
for (PluginMessagePacket message : loginPluginMessages) {
|
PluginMessagePacket message;
|
||||||
|
while ((message = loginPluginMessages.poll()) != null) {
|
||||||
ReferenceCountUtil.release(message);
|
ReferenceCountUtil.release(message);
|
||||||
}
|
}
|
||||||
|
loginPluginMessagesBytes.set(0);
|
||||||
|
loginPluginMessagesCount.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a retained plugin message to the queue used while the FML/login phases are still in
|
||||||
|
* progress, enforcing the per-connection byte and count caps. Returns {@code true} if queued,
|
||||||
|
* {@code false} if the packet was released (and the player disconnected on overflow).
|
||||||
|
*/
|
||||||
|
private boolean enqueueLoginPluginMessage(PluginMessagePacket packet) {
|
||||||
|
if (loginPluginMessagesOverflowed) {
|
||||||
|
ReferenceCountUtil.release(packet);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int packetSize = packet.content().readableBytes();
|
||||||
|
long newBytes = loginPluginMessagesBytes.addAndGet(packetSize);
|
||||||
|
int newCount = loginPluginMessagesCount.incrementAndGet();
|
||||||
|
if (newBytes > MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES
|
||||||
|
|| newCount > MAX_QUEUED_LOGIN_PLUGIN_MESSAGES) {
|
||||||
|
loginPluginMessagesOverflowed = true;
|
||||||
|
ReferenceCountUtil.release(packet);
|
||||||
|
logger.warn("Disconnecting {}: pre-join plugin-message queue exceeded its limits "
|
||||||
|
+ "({} messages, {} bytes).", player, newCount, newBytes);
|
||||||
|
player.disconnect(Component.translatable("velocity.error.plugin-message-overflow"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
loginPluginMessages.add(packet);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -201,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()) {
|
||||||
@@ -349,9 +407,26 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!player.getPhase().handle(player, packet, serverConn)) {
|
if (!player.getPhase().handle(player, packet, serverConn)) {
|
||||||
|
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||||
|
if (id == null) {
|
||||||
|
// We don't have any plugins listening on this channel, process the packet now.
|
||||||
|
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
||||||
|
.consideredComplete()) {
|
||||||
|
// The client is trying to send messages too early. This is primarily caused by mods,
|
||||||
|
// but further aggravated by Velocity. To work around these issues, we will queue any
|
||||||
|
// non-FML handshake messages to be sent once the FML handshake has completed or the
|
||||||
|
// JoinGame packet has been received by the proxy, whichever comes first.
|
||||||
|
//
|
||||||
|
// We also need to make sure to retain these packets, so they can be flushed
|
||||||
|
// appropriately.
|
||||||
|
enqueueLoginPluginMessage(packet.retain());
|
||||||
|
} else {
|
||||||
|
// The connection is ready, send the packet now.
|
||||||
|
backendConn.write(packet.retain());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||||
String channel = packet.getChannel();
|
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
|
||||||
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, 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()) {
|
if (pme.getResult().isAllowed()) {
|
||||||
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
|
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
|
||||||
@@ -359,7 +434,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
||||||
.consideredComplete()) {
|
.consideredComplete()) {
|
||||||
// We're still processing the connection (see above), enqueue the packet for now.
|
// We're still processing the connection (see above), enqueue the packet for now.
|
||||||
loginPluginMessages.add(message.retain());
|
enqueueLoginPluginMessage(message.retain());
|
||||||
} else {
|
} else {
|
||||||
backendConn.write(message);
|
backendConn.write(message);
|
||||||
}
|
}
|
||||||
@@ -371,6 +446,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -621,6 +697,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
while ((pm = loginPluginMessages.poll()) != null) {
|
while ((pm = loginPluginMessages.poll()) != null) {
|
||||||
serverMc.delayedWrite(pm);
|
serverMc.delayedWrite(pm);
|
||||||
}
|
}
|
||||||
|
loginPluginMessagesBytes.set(0);
|
||||||
|
loginPluginMessagesCount.set(0);
|
||||||
|
|
||||||
// Clear any title from the previous server.
|
// Clear any title from the previous server.
|
||||||
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
|
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
|
||||||
@@ -853,6 +931,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
while ((pm = loginPluginMessages.poll()) != null) {
|
while ((pm = loginPluginMessages.poll()) != null) {
|
||||||
connection.write(pm);
|
connection.write(pm);
|
||||||
}
|
}
|
||||||
|
loginPluginMessagesBytes.set(0);
|
||||||
|
loginPluginMessagesCount.set(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-23
@@ -84,7 +84,6 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
|
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
|
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
|
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
|
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
|
||||||
@@ -111,6 +110,7 @@ import java.util.Collections;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -118,13 +118,10 @@ import java.util.concurrent.CompletableFuture;
|
|||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import net.kyori.adventure.audience.MessageType;
|
|
||||||
import net.kyori.adventure.bossbar.BossBar;
|
import net.kyori.adventure.bossbar.BossBar;
|
||||||
import net.kyori.adventure.identity.Identity;
|
import net.kyori.adventure.identity.Identity;
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
import net.kyori.adventure.permission.PermissionChecker;
|
import net.kyori.adventure.permission.PermissionChecker;
|
||||||
import net.kyori.adventure.platform.facet.FacetPointers;
|
|
||||||
import net.kyori.adventure.platform.facet.FacetPointers.Type;
|
|
||||||
import net.kyori.adventure.pointer.Pointers;
|
import net.kyori.adventure.pointer.Pointers;
|
||||||
import net.kyori.adventure.pointer.PointersSupplier;
|
import net.kyori.adventure.pointer.PointersSupplier;
|
||||||
import net.kyori.adventure.resource.ResourcePackInfoLike;
|
import net.kyori.adventure.resource.ResourcePackInfoLike;
|
||||||
@@ -166,7 +163,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
.resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername()))
|
.resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername()))
|
||||||
.resolving(Identity.LOCALE, Player::getEffectiveLocale)
|
.resolving(Identity.LOCALE, Player::getEffectiveLocale)
|
||||||
.resolving(PermissionChecker.POINTER, Player::getPermissionChecker)
|
.resolving(PermissionChecker.POINTER, Player::getPermissionChecker)
|
||||||
.resolving(FacetPointers.TYPE, player -> Type.PLAYER)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -424,29 +420,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendMessage(@NonNull Identity identity, @NonNull Component message) {
|
public void sendMessage(final @NonNull Component message) {
|
||||||
|
Preconditions.checkNotNull(message, "message");
|
||||||
final Component translated = translateMessage(message);
|
final Component translated = translateMessage(message);
|
||||||
|
|
||||||
connection.write(getChatBuilderFactory().builder()
|
connection.write(getChatBuilderFactory().builder()
|
||||||
.component(translated).forIdentity(identity).toClient());
|
.component(translated).toClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendMessage(@NonNull Identity identity, @NonNull Component message,
|
public void sendActionBar(@NonNull Component message) {
|
||||||
@NonNull MessageType type) {
|
|
||||||
Preconditions.checkNotNull(message, "message");
|
|
||||||
Preconditions.checkNotNull(type, "type");
|
|
||||||
|
|
||||||
Component translated = translateMessage(message);
|
|
||||||
|
|
||||||
connection.write(getChatBuilderFactory().builder()
|
|
||||||
.component(translated).forIdentity(identity)
|
|
||||||
.setType(type == MessageType.CHAT ? ChatType.CHAT : ChatType.SYSTEM)
|
|
||||||
.toClient());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendActionBar(net.kyori.adventure.text.@NonNull Component message) {
|
|
||||||
Component translated = translateMessage(message);
|
Component translated = translateMessage(message);
|
||||||
|
|
||||||
ProtocolVersion playerVersion = getProtocolVersion();
|
ProtocolVersion playerVersion = getProtocolVersion();
|
||||||
@@ -645,7 +628,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void disconnect(Component reason) {
|
public void disconnect(@NotNull Component reason) {
|
||||||
|
Objects.requireNonNull(reason, "reason");
|
||||||
if (connection.eventLoop().inEventLoop()) {
|
if (connection.eventLoop().inEventLoop()) {
|
||||||
disconnect0(reason, false);
|
disconnect0(reason, false);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-2
@@ -65,8 +65,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||||
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id,
|
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
|
||||||
copy);
|
|
||||||
server.getEventManager().fire(event)
|
server.getEventManager().fire(event)
|
||||||
.thenAcceptAsync(pme -> {
|
.thenAcceptAsync(pme -> {
|
||||||
if (pme.getResult().isAllowed() && serverConn.isActive()) {
|
if (pme.getResult().isAllowed() && serverConn.isActive()) {
|
||||||
|
|||||||
+9
-22
@@ -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);
|
||||||
@@ -268,14 +261,8 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
|
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
|
||||||
}
|
}
|
||||||
}, mcConnection.eventLoop())
|
}, mcConnection.eventLoop())
|
||||||
.thenRun(() -> {
|
.whenComplete((ignored, throwable) -> {
|
||||||
try {
|
|
||||||
httpClient.close();
|
httpClient.close();
|
||||||
} catch (Exception e) {
|
|
||||||
// In Java 21, the HttpClient does not throw any Exception
|
|
||||||
// when trying to clean its resources, so this should not happen
|
|
||||||
logger.error("An unknown error occurred while trying to close an HttpClient", e);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
logger.error("Unable to enable encryption", e);
|
logger.error("Unable to enable encryption", e);
|
||||||
@@ -286,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());
|
||||||
|
|||||||
+1
-1
@@ -66,7 +66,7 @@ public class ServerListPingHandler {
|
|||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Collections.shuffle(unshuffledPlayers);
|
Collections.shuffle(unshuffledPlayers);
|
||||||
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount()));
|
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, unshuffledPlayers.size()));
|
||||||
} else {
|
} else {
|
||||||
samplePlayers = ImmutableList.of();
|
samplePlayers = ImmutableList.of();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,13 +25,11 @@ import com.velocitypowered.api.permission.Tristate;
|
|||||||
import com.velocitypowered.api.proxy.ConsoleCommandSource;
|
import com.velocitypowered.api.proxy.ConsoleCommandSource;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
|
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import net.kyori.adventure.audience.MessageType;
|
|
||||||
import net.kyori.adventure.identity.Identity;
|
import net.kyori.adventure.identity.Identity;
|
||||||
import net.kyori.adventure.permission.PermissionChecker;
|
import net.kyori.adventure.permission.PermissionChecker;
|
||||||
import net.kyori.adventure.platform.facet.FacetPointers;
|
|
||||||
import net.kyori.adventure.platform.facet.FacetPointers.Type;
|
|
||||||
import net.kyori.adventure.pointer.Pointers;
|
import net.kyori.adventure.pointer.Pointers;
|
||||||
import net.kyori.adventure.pointer.PointersSupplier;
|
import net.kyori.adventure.pointer.PointersSupplier;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
@@ -64,7 +62,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
|||||||
.resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker)
|
.resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker)
|
||||||
.resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE
|
.resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE
|
||||||
.lookupClosest(Locale.getDefault()))
|
.lookupClosest(Locale.getDefault()))
|
||||||
.resolving(FacetPointers.TYPE, (console) -> Type.CONSOLE)
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
public VelocityConsole(VelocityServer server) {
|
public VelocityConsole(VelocityServer server) {
|
||||||
@@ -72,8 +69,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendMessage(@NonNull Identity identity, @NonNull Component message,
|
public void sendMessage(@NonNull Component message) {
|
||||||
@NonNull MessageType messageType) {
|
|
||||||
componentLogger.info(message);
|
componentLogger.info(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +107,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
|||||||
protected LineReader buildReader(LineReaderBuilder builder) {
|
protected LineReader buildReader(LineReaderBuilder builder) {
|
||||||
return super.buildReader(builder
|
return super.buildReader(builder
|
||||||
.appName("Velocity")
|
.appName("Velocity")
|
||||||
|
.variable(LineReader.HISTORY_FILE, Path.of(".console_history"))
|
||||||
.completer((reader, parsedLine, list) -> {
|
.completer((reader, parsedLine, list) -> {
|
||||||
try {
|
try {
|
||||||
List<String> offers = this.server.getCommandManager()
|
List<String> offers = this.server.getCommandManager()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+12
-5
@@ -32,17 +32,20 @@ import java.net.InetAddress;
|
|||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@code InetNameResolver} that performs blocking DNS name lookups
|
* An implementation of {@code InetNameResolver} that performs blocking DNS name lookups
|
||||||
* in a separate thread, avoiding blocking the Netty threads for an extended period of time
|
* on a small bounded pool of separate threads, avoiding blocking the Netty threads for an
|
||||||
* and without the downsides of Netty's native DNS resolver.
|
* extended period of time and without the downsides of Netty's native DNS resolver.
|
||||||
*/
|
*/
|
||||||
public final class SeparatePoolInetNameResolver extends InetNameResolver {
|
public final class SeparatePoolInetNameResolver extends InetNameResolver {
|
||||||
|
|
||||||
|
private static final int MAX_RESOLVE_THREADS = 8;
|
||||||
|
|
||||||
private final ExecutorService resolveExecutor;
|
private final ExecutorService resolveExecutor;
|
||||||
private final InetNameResolver delegate;
|
private final InetNameResolver delegate;
|
||||||
private final Cache<String, List<InetAddress>> cache;
|
private final Cache<String, List<InetAddress>> cache;
|
||||||
@@ -56,11 +59,15 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
|
|||||||
*/
|
*/
|
||||||
public SeparatePoolInetNameResolver(EventExecutor executor) {
|
public SeparatePoolInetNameResolver(EventExecutor executor) {
|
||||||
super(executor);
|
super(executor);
|
||||||
this.resolveExecutor = Executors.newSingleThreadExecutor(
|
ThreadPoolExecutor resolveExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_RESOLVE_THREADS, MAX_RESOLVE_THREADS,
|
||||||
|
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
|
||||||
new ThreadFactoryBuilder()
|
new ThreadFactoryBuilder()
|
||||||
.setNameFormat("Velocity DNS Resolver")
|
.setNameFormat("Velocity DNS Resolver #%d")
|
||||||
.setDaemon(true)
|
.setDaemon(true)
|
||||||
.build());
|
.build());
|
||||||
|
resolveExecutor.allowCoreThreadTimeOut(true);
|
||||||
|
this.resolveExecutor = resolveExecutor;
|
||||||
this.delegate = new DefaultNameResolver(executor);
|
this.delegate = new DefaultNameResolver(executor);
|
||||||
this.cache = Caffeine.newBuilder()
|
this.cache = Caffeine.newBuilder()
|
||||||
.expireAfterWrite(30, TimeUnit.SECONDS)
|
.expireAfterWrite(30, TimeUnit.SECONDS)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+13
-1
@@ -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
|
||||||
+ '}';
|
+ '}';
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-3
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-4
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ public enum ProtocolUtils {
|
|||||||
|
|
||||||
private static final GsonComponentSerializer PRE_1_16_SERIALIZER =
|
private static final GsonComponentSerializer PRE_1_16_SERIALIZER =
|
||||||
GsonComponentSerializer.builder()
|
GsonComponentSerializer.builder()
|
||||||
.downsampleColors()
|
|
||||||
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
|
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
|
||||||
.options(
|
.options(
|
||||||
OptionSchema.globalSchema().stateBuilder()
|
OptionSchema.globalSchema().stateBuilder()
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -345,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,
|
||||||
@@ -835,24 +812,27 @@ public enum StateRegistry {
|
|||||||
ClientboundServerLinksPacket::new,
|
ClientboundServerLinksPacket::new,
|
||||||
map(0x7B, MINECRAFT_1_21, false),
|
map(0x7B, MINECRAFT_1_21, false),
|
||||||
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),
|
||||||
clientbound.register(UpdateTeamsPacket.class, UpdateTeamsPacket::new,
|
|
||||||
map(0x41, ProtocolVersion.MINECRAFT_1_9, true),
|
|
||||||
map(0x43, ProtocolVersion.MINECRAFT_1_12, true),
|
|
||||||
map(0x44, ProtocolVersion.MINECRAFT_1_12_1, true),
|
|
||||||
map(0x47, ProtocolVersion.MINECRAFT_1_13, true),
|
|
||||||
map(0x4B, ProtocolVersion.MINECRAFT_1_14, true),
|
|
||||||
map(0x4C, ProtocolVersion.MINECRAFT_1_15, true),
|
|
||||||
map(0x55, ProtocolVersion.MINECRAFT_1_17, true),
|
|
||||||
map(0x58, ProtocolVersion.MINECRAFT_1_19_1, true),
|
|
||||||
map(0x56, ProtocolVersion.MINECRAFT_1_19_3, true),
|
|
||||||
map(0x5A, ProtocolVersion.MINECRAFT_1_19_4, true),
|
|
||||||
map(0x5C, ProtocolVersion.MINECRAFT_1_20_2, true),
|
|
||||||
map(0x5E, ProtocolVersion.MINECRAFT_1_20_3, true),
|
|
||||||
map(0x60, ProtocolVersion.MINECRAFT_1_20_5, true),
|
|
||||||
map(0x67, ProtocolVersion.MINECRAFT_1_21_2, true),
|
|
||||||
map(0x6B, 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 {
|
||||||
|
|||||||
+17
-6
@@ -22,11 +22,14 @@ import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
|
|||||||
import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame;
|
import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame;
|
||||||
|
|
||||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||||
|
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
|
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.handler.codec.MessageToMessageDecoder;
|
import io.netty.handler.codec.MessageToMessageDecoder;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decompresses a Minecraft packet.
|
* Decompresses a Minecraft packet.
|
||||||
@@ -44,11 +47,12 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
|||||||
Boolean.getBoolean("velocity.increased-compression-cap")
|
Boolean.getBoolean("velocity.increased-compression-cap")
|
||||||
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE;
|
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE;
|
||||||
private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation");
|
private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation");
|
||||||
private static final double MAX_COMPRESSION_RATIO = Double.parseDouble(System.getProperty("velocity.max-compression-ratio", "64"));
|
|
||||||
private final ProtocolUtils.Direction direction;
|
private final ProtocolUtils.Direction direction;
|
||||||
|
|
||||||
private int threshold;
|
private int threshold;
|
||||||
private final VelocityCompressor compressor;
|
private final VelocityCompressor compressor;
|
||||||
|
@Nullable
|
||||||
|
private PacketLimiter packetLimiter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
|
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
|
||||||
@@ -73,10 +77,13 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
|||||||
+ " threshold %s", actualUncompressedSize, threshold);
|
+ " threshold %s", actualUncompressedSize, threshold);
|
||||||
}
|
}
|
||||||
// This message is not compressed.
|
// This message is not compressed.
|
||||||
|
if (packetLimiter != null && !packetLimiter.account(in.readableBytes())) {
|
||||||
|
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
|
||||||
|
.formatted(ctx.channel().remoteAddress()));
|
||||||
|
}
|
||||||
out.add(in.retain());
|
out.add(in.retain());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int length = in.readableBytes();
|
|
||||||
|
|
||||||
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
|
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
|
||||||
+ " threshold %s", claimedUncompressedSize, threshold);
|
+ " threshold %s", claimedUncompressedSize, threshold);
|
||||||
@@ -88,10 +95,6 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
|||||||
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
|
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
|
||||||
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
|
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
|
||||||
SERVERBOUND_UNCOMPRESSED_CAP);
|
SERVERBOUND_UNCOMPRESSED_CAP);
|
||||||
double maxCompressedAllowed = length * MAX_COMPRESSION_RATIO;
|
|
||||||
checkFrame(claimedUncompressedSize <= maxCompressedAllowed,
|
|
||||||
"Uncompressed size %s exceeds ratio threshold of %s for compressed sized %s", claimedUncompressedSize,
|
|
||||||
maxCompressedAllowed, length);
|
|
||||||
}
|
}
|
||||||
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
||||||
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
|
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
|
||||||
@@ -99,6 +102,10 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
|||||||
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
|
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
|
||||||
checkFrame(uncompressed.writerIndex() == claimedUncompressedSize,
|
checkFrame(uncompressed.writerIndex() == claimedUncompressedSize,
|
||||||
"Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize);
|
"Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize);
|
||||||
|
if (packetLimiter != null && !packetLimiter.account(claimedUncompressedSize)) {
|
||||||
|
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
|
||||||
|
.formatted(ctx.channel().remoteAddress()));
|
||||||
|
}
|
||||||
out.add(uncompressed);
|
out.add(uncompressed);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
uncompressed.release();
|
uncompressed.release();
|
||||||
@@ -116,4 +123,8 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
|||||||
public void setThreshold(int threshold) {
|
public void setThreshold(int threshold) {
|
||||||
this.threshold = threshold;
|
this.threshold = threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
|
||||||
|
this.packetLimiter = packetLimiter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
|||||||
throw handleOverflow(packet, expectedMaxLen, buf.readableBytes());
|
throw handleOverflow(packet, expectedMaxLen, buf.readableBytes());
|
||||||
}
|
}
|
||||||
if (buf.readableBytes() < expectedMinLen) {
|
if (buf.readableBytes() < expectedMinLen) {
|
||||||
throw handleUnderflow(packet, expectedMaxLen, buf.readableBytes());
|
throw handleUnderflow(packet, expectedMinLen, buf.readableBytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -159,10 +159,10 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
|||||||
int expectedMinLen = packet.decodeExpectedMinLength(in, direction, registry.version);
|
int expectedMinLen = packet.decodeExpectedMinLength(in, direction, registry.version);
|
||||||
int expectedMaxLen = packet.decodeExpectedMaxLength(in, direction, registry.version);
|
int expectedMaxLen = packet.decodeExpectedMaxLength(in, direction, registry.version);
|
||||||
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
|
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
|
||||||
throw handleOverflow(packet, expectedMaxLen, in.readableBytes());
|
throw handleOverflow(packet, expectedMaxLen, payloadLength);
|
||||||
}
|
}
|
||||||
if (payloadLength < expectedMinLen) {
|
if (payloadLength < expectedMinLen) {
|
||||||
throw handleUnderflow(packet, expectedMaxLen, in.readableBytes());
|
throw handleUnderflow(packet, expectedMinLen, payloadLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
in.readerIndex(index);
|
in.readerIndex(index);
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ public class JoinGamePacket implements MinecraftPacket {
|
|||||||
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
|
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
|
||||||
private int portalCooldown; // 1.20+
|
private int portalCooldown; // 1.20+
|
||||||
private int seaLevel; // 1.21.2+
|
private int seaLevel; // 1.21.2+
|
||||||
|
private boolean onlineMode; // 26.2+
|
||||||
private boolean enforcesSecureChat; // 1.20.5+
|
private boolean enforcesSecureChat; // 1.20.5+
|
||||||
|
|
||||||
public int getEntityId() {
|
public int getEntityId() {
|
||||||
@@ -190,6 +191,10 @@ public class JoinGamePacket implements MinecraftPacket {
|
|||||||
this.seaLevel = seaLevel;
|
this.seaLevel = seaLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOnlineMode(boolean onlineMode) {
|
||||||
|
this.onlineMode = onlineMode;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getEnforcesSecureChat() {
|
public boolean getEnforcesSecureChat() {
|
||||||
return this.enforcesSecureChat;
|
return this.enforcesSecureChat;
|
||||||
}
|
}
|
||||||
@@ -213,7 +218,7 @@ public class JoinGamePacket implements MinecraftPacket {
|
|||||||
dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' +
|
dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' +
|
||||||
", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance +
|
", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance +
|
||||||
", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown +
|
", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown +
|
||||||
", seaLevel=" + seaLevel +
|
", seaLevel=" + seaLevel + ", onlineMode=" + this.onlineMode +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +363,10 @@ public class JoinGamePacket implements MinecraftPacket {
|
|||||||
this.seaLevel = ProtocolUtils.readVarInt(buf);
|
this.seaLevel = ProtocolUtils.readVarInt(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
|
||||||
|
this.onlineMode = buf.readBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
|
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
|
||||||
this.enforcesSecureChat = buf.readBoolean();
|
this.enforcesSecureChat = buf.readBoolean();
|
||||||
}
|
}
|
||||||
@@ -510,6 +519,10 @@ public class JoinGamePacket implements MinecraftPacket {
|
|||||||
ProtocolUtils.writeVarInt(buf, seaLevel);
|
ProtocolUtils.writeVarInt(buf, seaLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
|
||||||
|
buf.writeBoolean(this.onlineMode);
|
||||||
|
}
|
||||||
|
|
||||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
|
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
|
||||||
buf.writeBoolean(this.enforcesSecureChat);
|
buf.writeBoolean(this.enforcesSecureChat);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -35,6 +35,7 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
|
|||||||
private @Nullable UUID uuid;
|
private @Nullable UUID uuid;
|
||||||
private @Nullable String username;
|
private @Nullable String username;
|
||||||
private @Nullable List<GameProfile.Property> properties;
|
private @Nullable List<GameProfile.Property> properties;
|
||||||
|
private @Nullable UUID sessionId;
|
||||||
private static final boolean strictErrorHandling = VelocityProperties
|
private static final boolean strictErrorHandling = VelocityProperties
|
||||||
.readBoolean("velocity.strictErrorHandling", true);
|
.readBoolean("velocity.strictErrorHandling", true);
|
||||||
|
|
||||||
@@ -68,6 +69,10 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
|
|||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSessionId(@Nullable UUID sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "ServerLoginSuccess{"
|
return "ServerLoginSuccess{"
|
||||||
@@ -96,6 +101,10 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
|
|||||||
if (version == ProtocolVersion.MINECRAFT_1_20_5 || version == ProtocolVersion.MINECRAFT_1_21) {
|
if (version == ProtocolVersion.MINECRAFT_1_20_5 || version == ProtocolVersion.MINECRAFT_1_21) {
|
||||||
buf.readBoolean();
|
buf.readBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
|
||||||
|
this.sessionId = ProtocolUtils.readUuid(buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -127,6 +136,10 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
|
|||||||
if (version == ProtocolVersion.MINECRAFT_1_20_5 || version == ProtocolVersion.MINECRAFT_1_21) {
|
if (version == ProtocolVersion.MINECRAFT_1_20_5 || version == ProtocolVersion.MINECRAFT_1_21) {
|
||||||
buf.writeBoolean(strictErrorHandling);
|
buf.writeBoolean(strictErrorHandling);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
|
||||||
|
ProtocolUtils.writeUuid(buf, this.sessionId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+50
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-4
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -24,6 +24,7 @@ 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_20_5;
|
||||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
|
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_6;
|
||||||
|
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_2;
|
||||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id;
|
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id;
|
||||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
|
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
|
||||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE;
|
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE;
|
||||||
@@ -207,7 +208,7 @@ public class ArgumentPropertyRegistry {
|
|||||||
empty(id("minecraft:block_predicate", mapSet(MINECRAFT_1_19, 13)));
|
empty(id("minecraft:block_predicate", mapSet(MINECRAFT_1_19, 13)));
|
||||||
empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14)));
|
empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14)));
|
||||||
empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15)));
|
empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15)));
|
||||||
empty(id("minecraft:color", mapSet(MINECRAFT_1_19, 16)));
|
empty(id("minecraft:color", mapSet(MINECRAFT_26_2, -1), mapSet(MINECRAFT_1_19, 16))); // renamed to team_color in 26.2
|
||||||
empty(id("minecraft:component", mapSet(MINECRAFT_1_21_6, 18), mapSet(MINECRAFT_1_19, 17)));
|
empty(id("minecraft:component", mapSet(MINECRAFT_1_21_6, 18), mapSet(MINECRAFT_1_19, 17)));
|
||||||
empty(id("minecraft:style", mapSet(MINECRAFT_1_21_6, 19), mapSet(MINECRAFT_1_20_3, 18))); // added 1.20.3
|
empty(id("minecraft:style", mapSet(MINECRAFT_1_21_6, 19), mapSet(MINECRAFT_1_20_3, 18))); // added 1.20.3
|
||||||
empty(id("minecraft:message", mapSet(MINECRAFT_1_21_6, 20), mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18)));
|
empty(id("minecraft:message", mapSet(MINECRAFT_1_21_6, 20), mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18)));
|
||||||
@@ -281,6 +282,7 @@ public class ArgumentPropertyRegistry {
|
|||||||
|
|
||||||
empty(id("minecraft:hex_color", mapSet(MINECRAFT_1_21_6, 17))); // added in 1.21.6
|
empty(id("minecraft:hex_color", mapSet(MINECRAFT_1_21_6, 17))); // added in 1.21.6
|
||||||
empty(id("minecraft:dialog", mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6
|
empty(id("minecraft:dialog", mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6
|
||||||
|
empty(id("minecraft:team_color", mapSet(MINECRAFT_26_2, 16))); // renamed from color in 26.2
|
||||||
|
|
||||||
// Crossstitch support
|
// Crossstitch support
|
||||||
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);
|
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);
|
||||||
|
|||||||
+1
-1
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ velocity.error.internal-server-connection-error=An internal server connection er
|
|||||||
velocity.error.logging-in-too-fast=You are logging in too fast, try again later.
|
velocity.error.logging-in-too-fast=You are logging in too fast, try again later.
|
||||||
velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client.
|
velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client.
|
||||||
velocity.error.player-connection-error=An internal error occurred in your connection.
|
velocity.error.player-connection-error=An internal error occurred in your connection.
|
||||||
|
velocity.error.plugin-message-overflow=You sent too many plugin messages before completing the connection.
|
||||||
velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above.
|
velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above.
|
||||||
velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding.
|
velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding.
|
||||||
velocity.error.moved-to-new-server=You were kicked from <arg:0>: <arg:1>
|
velocity.error.moved-to-new-server=You were kicked from <arg:0>: <arg:1>
|
||||||
@@ -49,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>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Config version. Do not change this
|
# Config version. Do not change this
|
||||||
config-version = "2.7"
|
config-version = "2.8"
|
||||||
|
|
||||||
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565.
|
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565.
|
||||||
bind = "0.0.0.0:25565"
|
bind = "0.0.0.0:25565"
|
||||||
@@ -75,9 +75,17 @@ sample-players-in-ping = false
|
|||||||
enable-player-address-logging = true
|
enable-player-address-logging = true
|
||||||
|
|
||||||
[packet-limiter]
|
[packet-limiter]
|
||||||
|
# Size of the moving time window in seconds used to calculate average rates.
|
||||||
|
# A larger window tolerates short bursts while still enforcing the configured limits over time.
|
||||||
interval = 7
|
interval = 7
|
||||||
packets-per-second = 500
|
# Maximum average number of packets per second a client may send. -1 disables this check.
|
||||||
|
packets-per-second = -1
|
||||||
|
# Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.
|
||||||
bytes-per-second = -1
|
bytes-per-second = -1
|
||||||
|
# Maximum average number of decompressed bytes per second a client may send.
|
||||||
|
# Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
|
||||||
|
# -1 disables this check.
|
||||||
|
decompressed-bytes-per-second = 5242880
|
||||||
|
|
||||||
[servers]
|
[servers]
|
||||||
# Configure your servers here. Each key represents the server's name, and the value
|
# Configure your servers here. Each key represents the server's name, and the value
|
||||||
|
|||||||
+119
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
-1
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user