3
0
Mirror von https://github.com/PaperMC/Velocity.git synchronisiert 2024-12-24 15:20:35 +01:00

Merge branch 'master' into native-crypto

# Conflicts:
#	native/src/main/java/com/velocitypowered/natives/util/Natives.java
Dieser Commit ist enthalten in:
Andrew Steinborn 2018-08-25 01:12:26 -04:00
Commit a37a0d6665
152 geänderte Dateien mit 6546 neuen und 866 gelöschten Zeilen

17
.gitignore vendored
Datei anzeigen

@ -11,6 +11,7 @@
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/compiler.xml
# Sensitive or high-churn files
.idea/**/dataSources/
@ -85,6 +86,15 @@ modules.xml
# BlueJ files
*.ctxt
# Eclipse #
**/.classpath
**/.project
**/.settings/
**/bin/
# NetBeans Gradle#
.nb-gradle/
# Mobile Tools for Java (J2ME)
.mtj.tmp/
@ -102,7 +112,7 @@ hs_err_pid*
### Gradle ###
.gradle
/build/
build/
# Ignore Gradle GUI config
gradle-app.setting
@ -121,4 +131,7 @@ gradle-app.setting
# Other trash
logs/
velocity.toml
/velocity.toml
server-icon.png
/bin/
run/

14
Jenkinsfile vendored
Datei anzeigen

@ -1,8 +1,8 @@
pipeline {
agent {
docker {
image 'openjdk:8-jdk-slim'
args '-v gradle-cache:/root/.gradle:rw'
image 'velocitypowered/openjdk8-plus-git:slim'
args '-v gradle-cache:/root/.gradle:rw -v maven-repo:/maven-repo:rw -v javadoc:/javadoc'
}
}
@ -18,5 +18,15 @@ pipeline {
sh './gradlew test'
}
}
stage('Deploy Artifacts') {
steps {
sh 'export MAVEN_DEPLOYMENT=true; ./gradlew publish'
}
}
stage('Deploy Javadoc') {
steps {
sh 'rsync -av --delete ./api/build/docs/javadoc/ /javadoc'
}
}
}
}

Datei anzeigen

@ -1,6 +1,7 @@
# Velocity
[![Build Status](https://img.shields.io/jenkins/s/https/ci.velocitypowered.com/job/velocity/job/master.svg)](https://ci.velocitypowered.com/job/velocity/job/master/)
[![Join our Discord](https://img.shields.io/discord/472484458856185878.svg?logo=discord&label=)](https://discord.gg/8cB9Bgf)
Velocity is a next-generation Minecraft: Java Edition proxy suite. It is
designed specifically with mass-scale Minecraft in mind.
@ -22,10 +23,17 @@ wrapper script (`./gradlew`) as our CI builds using it.
It is sufficient to run `./gradlew build` to run the full build cycle.
## Running
Once you've built Velocity, you can copy and run the `-all` JAR from
`proxy/build/libs`. Velocity will generate a default configuration file
and you can configure it from there.
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
page.
## Status
Velocity is far from finished, but most of the essential pieces are in place:
you can switch between two servers running Minecraft 1.9-1.13. More versions
and functionality is planned.
You should join us on **irc.spi.gt** `#velocity` or send us a pull request.
Velocity is far from finished, but most of the essential pieces you would
expect are in place. Velocity supports Minecraft 1.8-1.13. More functionality
is planned.

Datei anzeigen

@ -1,13 +1,24 @@
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '2.0.4'
id 'maven-publish'
}
sourceSets {
ap {
compileClasspath += main.compileClasspath + main.output
}
}
dependencies {
compile 'com.google.code.gson:gson:2.8.5'
compile "com.google.guava:guava:${guavaVersion}"
compile 'net.kyori:text:1.12-1.6.0-SNAPSHOT'
compile 'net.kyori:text:1.12-1.6.4'
compile 'com.moandjiezana.toml:toml4j:0.7.2'
compile "org.slf4j:slf4j-api:${slf4jVersion}"
compile 'com.google.inject:guice:4.2.0'
compile 'org.checkerframework:checker-qual:2.5.4'
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
}
@ -20,10 +31,55 @@ task javadocJar(type: Jar) {
task sourcesJar(type: Jar) {
classifier 'sources'
from sourceSets.main.allSource
from sourceSets.ap.output
}
jar {
from sourceSets.ap.output
}
shadowJar {
from sourceSets.ap.output
}
artifacts {
archives javadocJar
archives shadowJar
archives sourcesJar
}
javadoc {
options.encoding = 'UTF-8'
options.charSet = 'UTF-8'
options.links(
'http://www.slf4j.org/apidocs/',
'https://google.github.io/guava/releases/25.1-jre/api/docs/',
'https://google.github.io/guice/api-docs/4.2/javadoc/',
'https://jd.kyori.net/text/1.12-1.6.4/',
'https://docs.oracle.com/javase/8/docs/api/'
)
// Disable the crazy super-strict doclint tool in Java 8
options.addStringOption('Xdoclint:none', '-quiet')
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
}
}
repositories {
maven {
name = 'myRepo'
def base = project.ext.getCurrentBranchName() == "master" ? 'file:///maven-repo' : File.createTempDir().toURI().toURL().toString()
def releasesRepoUrl = "$base/releases"
def snapshotsRepoUrl = "$base/snapshots"
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
}
}
}

Datei anzeigen

@ -0,0 +1,87 @@
package com.velocitypowered.api.plugin.ap;
import com.google.gson.Gson;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.PluginDescription;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
@SupportedAnnotationTypes({"com.velocitypowered.api.plugin.Plugin"})
public class PluginAnnotationProcessor extends AbstractProcessor {
private ProcessingEnvironment environment;
private String pluginClassFound;
private boolean warnedAboutMultiplePlugins;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
this.environment = processingEnv;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
return false;
}
for (Element element : roundEnv.getElementsAnnotatedWith(Plugin.class)) {
if (element.getKind() != ElementKind.CLASS) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Only classes can be annotated with "
+ Plugin.class.getCanonicalName());
return false;
}
Name qualifiedName = ((TypeElement) element).getQualifiedName();
if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
if (!warnedAboutMultiplePlugins) {
environment.getMessager().printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support " +
"multiple plugins. We are using " + pluginClassFound + " for your plugin's main class.");
warnedAboutMultiplePlugins = true;
}
return false;
}
Plugin plugin = element.getAnnotation(Plugin.class);
if (!PluginDescription.ID_PATTERN.matcher(plugin.id()).matches()) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Invalid ID for plugin "
+ qualifiedName + ". IDs must start alphabetically, have alphanumeric characters, and can " +
"contain dashes or underscores.");
return false;
}
// All good, generate the velocity-plugin.json.
SerializedPluginDescription description = SerializedPluginDescription.from(plugin, qualifiedName.toString());
try {
FileObject object = environment.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", "velocity-plugin.json");
try (Writer writer = new BufferedWriter(object.openWriter())) {
new Gson().toJson(description, writer);
}
pluginClassFound = qualifiedName.toString();
} catch (IOException e) {
environment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to generate plugin file");
}
}
return false;
}
}

Datei anzeigen

@ -0,0 +1,151 @@
package com.velocitypowered.api.plugin.ap;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.velocitypowered.api.plugin.Plugin;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class SerializedPluginDescription {
// @Nullable is used here to make GSON skip these in the serialized file
private final String id;
private final @Nullable String name;
private final @Nullable String version;
private final @Nullable String description;
private final @Nullable String url;
private final @Nullable List<String> authors;
private final @Nullable List<Dependency> dependencies;
private final String main;
public SerializedPluginDescription(String id, String name, String version, String description, String url,
List<String> authors, List<Dependency> dependencies, String main) {
this.id = Preconditions.checkNotNull(id, "id");
this.name = Strings.emptyToNull(name);
this.version = Strings.emptyToNull(version);
this.description = Strings.emptyToNull(description);
this.url = Strings.emptyToNull(url);
this.authors = authors == null || authors.isEmpty() ? null : authors;
this.dependencies = dependencies == null || dependencies.isEmpty() ? null : dependencies;
this.main = Preconditions.checkNotNull(main, "main");
}
public static SerializedPluginDescription from(Plugin plugin, String qualifiedName) {
List<Dependency> dependencies = new ArrayList<>();
for (com.velocitypowered.api.plugin.Dependency dependency : plugin.dependencies()) {
dependencies.add(new Dependency(dependency.id(), dependency.optional()));
}
return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), plugin.description(), plugin.url(),
Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty()).collect(Collectors.toList()), dependencies, qualifiedName);
}
public String getId() {
return id;
}
public @Nullable String getName() {
return name;
}
public @Nullable String getVersion() {
return version;
}
public @Nullable String getDescription() {
return description;
}
public @Nullable String getUrl() {
return url;
}
public @Nullable List<String> getAuthors() {
return authors;
}
public @Nullable List<Dependency> getDependencies() {
return dependencies;
}
public String getMain() {
return main;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SerializedPluginDescription that = (SerializedPluginDescription) o;
return Objects.equals(id, that.id) &&
Objects.equals(name, that.name) &&
Objects.equals(version, that.version) &&
Objects.equals(description, that.description) &&
Objects.equals(url, that.url) &&
Objects.equals(authors, that.authors) &&
Objects.equals(dependencies, that.dependencies) &&
Objects.equals(main, that.main);
}
@Override
public int hashCode() {
return Objects.hash(id, name, version, description, url, authors, dependencies);
}
@Override
public String toString() {
return "SerializedPluginDescription{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", version='" + version + '\'' +
", description='" + description + '\'' +
", url='" + url + '\'' +
", authors=" + authors +
", dependencies=" + dependencies +
", main='" + main + '\'' +
'}';
}
public static class Dependency {
private final String id;
private final boolean optional;
public Dependency(String id, boolean optional) {
this.id = id;
this.optional = optional;
}
public String getId() {
return id;
}
public boolean isOptional() {
return optional;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dependency that = (Dependency) o;
return optional == that.optional &&
Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id, optional);
}
@Override
public String toString() {
return "Dependency{" +
"id='" + id + '\'' +
", optional=" + optional +
'}';
}
}
}

Datei anzeigen

@ -0,0 +1 @@
com.velocitypowered.api.plugin.ap.PluginAnnotationProcessor

Datei anzeigen

@ -0,0 +1,29 @@
package com.velocitypowered.api.command;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
/**
* Represents a command that can be executed by a {@link CommandSource}, such as a {@link com.velocitypowered.api.proxy.Player}
* or the console.
*/
public interface Command {
/**
* Executes the command for the specified {@link CommandSource}.
* @param source the source of this command
* @param args the arguments for this command
*/
void execute(@NonNull CommandSource source, @NonNull String[] args);
/**
* Provides tab complete suggestions for a command for a specified {@link CommandSource}.
* @param source the source to run the command for
* @param currentArgs the current, partial arguments for this command
* @return tab complete suggestions
*/
default List<String> suggest(@NonNull CommandSource source, @NonNull String[] currentArgs) {
return ImmutableList.of();
}
}

Datei anzeigen

@ -0,0 +1,29 @@
package com.velocitypowered.api.command;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Represents an interface to register a command executor with the proxy.
*/
public interface CommandManager {
/**
* Registers the specified command with the manager with the specified aliases.
* @param command the command to register
* @param aliases the alias to use
*/
void register(@NonNull Command command, String... aliases);
/**
* Unregisters a command.
* @param alias the command alias to unregister
*/
void unregister(@NonNull String alias);
/**
* Attempts to execute a command from the specified {@code cmdLine}.
* @param source the command's source
* @param cmdLine the command to run
* @return true if the command was found and executed, false if it was not
*/
boolean execute(@NonNull CommandSource source, @NonNull String cmdLine);
}

Datei anzeigen

@ -0,0 +1,16 @@
package com.velocitypowered.api.command;
import com.velocitypowered.api.permission.PermissionSubject;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Represents something that can be used to run a {@link Command}.
*/
public interface CommandSource extends PermissionSubject {
/**
* Sends the specified {@code component} to the invoker.
* @param component the text component to send
*/
void sendMessage(@NonNull Component component);
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides a simple command framework.
*/
package com.velocitypowered.api.command;

Datei anzeigen

@ -0,0 +1,12 @@
package com.velocitypowered.api.event;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Represents an interface to perform direct dispatch of an event. This makes integration easier to achieve with platforms
* such as RxJava.
*/
@FunctionalInterface
public interface EventHandler<E> {
void execute(@NonNull E event);
}

Datei anzeigen

@ -0,0 +1,75 @@
package com.velocitypowered.api.event;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.concurrent.CompletableFuture;
/**
* Allows plugins to register and deregister listeners for event handlers.
*/
public interface EventManager {
/**
* Requests that the specified {@code listener} listen for events and associate it with the {@code plugin}.
* @param plugin the plugin to associate with the listener
* @param listener the listener to register
*/
void register(@NonNull Object plugin, @NonNull Object listener);
/**
* Requests that the specified {@code handler} listen for events and associate it with the {@code plugin}.
* @param plugin the plugin to associate with the handler
* @param eventClass the class for the event handler to register
* @param handler the handler to register
* @param <E> the event type to handle
*/
default <E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull EventHandler<E> handler) {
register(plugin, eventClass, PostOrder.NORMAL, handler);
}
/**
* Requests that the specified {@code handler} listen for events and associate it with the {@code plugin}.
* @param plugin the plugin to associate with the handler
* @param eventClass the class for the event handler to register
* @param postOrder the order in which events should be posted to the handler
* @param handler the handler to register
* @param <E> the event type to handle
*/
<E> void register(@NonNull Object plugin, @NonNull Class<E> eventClass, @NonNull PostOrder postOrder, @NonNull EventHandler<E> handler);
/**
* Fires the specified event to the event bus asynchronously. This allows Velocity to continue servicing connections
* while a plugin handles a potentially long-running operation such as a database query.
* @param event the event to fire
* @return a {@link CompletableFuture} representing the posted event
*/
@NonNull <E> CompletableFuture<E> fire(@NonNull E event);
/**
* Posts the specified event to the event bus and discards the result.
* @param event the event to fire
*/
default void fireAndForget(@NonNull Object event) {
fire(event);
}
/**
* Unregisters all listeners for the specified {@code plugin}.
* @param plugin the plugin to deregister listeners for
*/
void unregisterListeners(@NonNull Object plugin);
/**
* Unregisters a specific listener for a specific plugin.
* @param plugin the plugin associated with the listener
* @param listener the listener to deregister
*/
void unregisterListener(@NonNull Object plugin, @NonNull Object listener);
/**
* Unregisters a specific event handler for a specific plugin.
* @param plugin the plugin to associate with the handler
* @param handler the handler to register
* @param <E> the event type to handle
*/
<E> void unregister(@NonNull Object plugin, @NonNull EventHandler<E> handler);
}

Datei anzeigen

@ -0,0 +1,11 @@
package com.velocitypowered.api.event;
/**
* Represents the order an event will be posted to a listener method, relative
* to other listeners.
*/
public enum PostOrder {
FIRST, EARLY, NORMAL, LATE, LAST;
}

Datei anzeigen

@ -0,0 +1,114 @@
package com.velocitypowered.api.event;
import com.google.common.base.Preconditions;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Optional;
/**
* Indicates an event that has a result attached to it.
*/
public interface ResultedEvent<R extends ResultedEvent.Result> {
/**
* Returns the result associated with this event.
* @return the result of this event
*/
R getResult();
/**
* Sets the result of this event. The result must be non-null.
* @param result the new result
*/
void setResult(@NonNull R result);
/**
* Represents a result for an event.
*/
interface Result {
/**
* Returns whether or not the event is allowed to proceed. Plugins may choose to skip denied events, and the
* proxy will respect the result of this method.
* @return whether or not the event is allowed to proceed
*/
boolean isAllowed();
}
/**
* A generic "allowed/denied" result.
*/
class GenericResult implements Result {
private static final GenericResult ALLOWED = new GenericResult(true);
private static final GenericResult DENIED = new GenericResult(true);
private final boolean allowed;
private GenericResult(boolean b) {
this.allowed = b;
}
@Override
public boolean isAllowed() {
return allowed;
}
@Override
public String toString() {
return allowed ? "allowed" : "denied";
}
public static GenericResult allowed() {
return ALLOWED;
}
public static GenericResult denied() {
return DENIED;
}
}
/**
* Represents an "allowed/denied" result with a reason allowed for denial.
*/
class ComponentResult implements Result {
private static final ComponentResult ALLOWED = new ComponentResult(true, null);
private final boolean allowed;
private final @Nullable Component reason;
protected ComponentResult(boolean allowed, @Nullable Component reason) {
this.allowed = allowed;
this.reason = reason;
}
@Override
public boolean isAllowed() {
return allowed;
}
public Optional<Component> getReason() {
return Optional.ofNullable(reason);
}
@Override
public String toString() {
if (allowed) {
return "allowed";
}
if (reason != null) {
return "denied: " + ComponentSerializers.PLAIN.serialize(reason);
}
return "denied";
}
public static ComponentResult allowed() {
return ALLOWED;
}
public static ComponentResult denied(@NonNull Component reason) {
Preconditions.checkNotNull(reason, "reason");
return new ComponentResult(false, reason);
}
}
}

Datei anzeigen

@ -0,0 +1,22 @@
package com.velocitypowered.api.event;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An annotation that indicates that this method can be used to listen for an event from the proxy.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscribe {
/**
* The order events will be posted to this listener.
*
* @return the order
*/
PostOrder order() default PostOrder.NORMAL;
}

Datei anzeigen

@ -0,0 +1,27 @@
package com.velocitypowered.api.event.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.InboundConnection;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* This event is fired when a handshake is established between a client and Velocity.
*/
public class ConnectionHandshakeEvent {
private final @NonNull InboundConnection connection;
public ConnectionHandshakeEvent(@NonNull InboundConnection connection) {
this.connection = Preconditions.checkNotNull(connection, "connection");
}
public InboundConnection getConnection() {
return connection;
}
@Override
public String toString() {
return "ConnectionHandshakeEvent{" +
"connection=" + connection +
'}';
}
}

Datei anzeigen

@ -0,0 +1,28 @@
package com.velocitypowered.api.event.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* This event is fired when a player disconnects from the proxy. Operations on the provided player, aside from basic
* data retrieval operations, may behave in undefined ways.
*/
public class DisconnectEvent {
private @NonNull final Player player;
public DisconnectEvent(@NonNull Player player) {
this.player = Preconditions.checkNotNull(player, "player");
}
public Player getPlayer() {
return player;
}
@Override
public String toString() {
return "DisconnectEvent{" +
"player=" + player +
'}';
}
}

Datei anzeigen

@ -0,0 +1,41 @@
package com.velocitypowered.api.event.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.Player;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* This event is fired once the player has been authenticated but before they connect to a server on the proxy.
*/
public class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
private final Player player;
private ComponentResult result;
public LoginEvent(@NonNull Player player) {
this.player = Preconditions.checkNotNull(player, "player");
this.result = ComponentResult.allowed();
}
public Player getPlayer() {
return player;
}
@Override
public ComponentResult getResult() {
return result;
}
@Override
public void setResult(@NonNull ComponentResult result) {
this.result = Preconditions.checkNotNull(result, "result");
}
@Override
public String toString() {
return "LoginEvent{" +
"player=" + player +
", result=" + result +
'}';
}
}

Datei anzeigen

@ -0,0 +1,119 @@
package com.velocitypowered.api.event.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* This event is fired when a player has initiated a connection with the proxy but before the proxy authenticates the
* player with Mojang or before the player's proxy connection is fully established (for offline mode).
*/
public class PreLoginEvent implements ResultedEvent<PreLoginEvent.PreLoginComponentResult> {
private final InboundConnection connection;
private final String username;
private PreLoginComponentResult result;
public PreLoginEvent(InboundConnection connection, String username) {
this.connection = Preconditions.checkNotNull(connection, "connection");
this.username = Preconditions.checkNotNull(username, "username");
this.result = PreLoginComponentResult.allowed();
}
public InboundConnection getConnection() {
return connection;
}
public String getUsername() {
return username;
}
@Override
public PreLoginComponentResult getResult() {
return result;
}
@Override
public void setResult(@NonNull PreLoginComponentResult result) {
this.result = Preconditions.checkNotNull(result, "result");
}
@Override
public String toString() {
return "PreLoginEvent{" +
"connection=" + connection +
", username='" + username + '\'' +
", result=" + result +
'}';
}
/**
* Represents an "allowed/allowed with online mode/denied" result with a reason allowed for denial.
*/
public static class PreLoginComponentResult extends ResultedEvent.ComponentResult {
private static final PreLoginComponentResult ALLOWED = new PreLoginComponentResult((Component) null);
private static final PreLoginComponentResult FORCE_ONLINEMODE = new PreLoginComponentResult(true);
private final boolean onlineMode;
/**
* Allows online mode to be enabled for the player connection, if Velocity is running in offline mode.
* @param allowedOnlineMode if true, online mode will be used for the connection
*/
private PreLoginComponentResult(boolean allowedOnlineMode) {
super(true, null);
this.onlineMode = allowedOnlineMode;
}
private PreLoginComponentResult(@Nullable Component reason) {
super(reason == null, reason);
// Don't care about this
this.onlineMode = false;
}
public boolean isOnlineModeAllowed() {
return this.onlineMode;
}
@Override
public String toString() {
if (isOnlineModeAllowed()) {
return "allowed with online mode";
}
return super.toString();
}
/**
* Returns a result indicating the connection will be allowed through the proxy.
* @return the allowed result
*/
public static PreLoginComponentResult allowed() {
return ALLOWED;
}
/**
* Returns a result indicating the connection will be allowed through the proxy, but the connection will be
* forced to use online mode provided that the proxy is in offline mode. This acts similarly to {@link #allowed()}
* on an online-mode proxy.
* @return the result
*/
public static PreLoginComponentResult forceOnlineMode() {
return FORCE_ONLINEMODE;
}
/**
* Denies the login with the specified reason.
* @param reason the reason for disallowing the connection
* @return a new result
*/
public static PreLoginComponentResult denied(@NonNull Component reason) {
Preconditions.checkNotNull(reason, "reason");
return new PreLoginComponentResult(reason);
}
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides events for handling incoming connections to the proxy and loigns.
*/
package com.velocitypowered.api.event.connection;

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides core support for handling events with Velocity. Subpackages include event classes.
*/
package com.velocitypowered.api.event;

Datei anzeigen

@ -0,0 +1,64 @@
package com.velocitypowered.api.event.permission;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.permission.PermissionFunction;
import com.velocitypowered.api.permission.PermissionProvider;
import com.velocitypowered.api.permission.PermissionSubject;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Called when a {@link PermissionSubject}'s permissions are being setup.
*
* <p>This event is only called once per subject, on initialisation.</p>
*/
public class PermissionsSetupEvent {
private final PermissionSubject subject;
private final PermissionProvider defaultProvider;
private PermissionProvider provider;
public PermissionsSetupEvent(PermissionSubject subject, PermissionProvider provider) {
this.subject = Preconditions.checkNotNull(subject, "subject");
this.provider = this.defaultProvider = Preconditions.checkNotNull(provider, "provider");
}
public @NonNull PermissionSubject getSubject() {
return this.subject;
}
/**
* Uses the provider function to obtain a {@link PermissionFunction} for
* the subject.
*
* @param subject the subject
* @return the obtained permission function
*/
public @NonNull PermissionFunction createFunction(PermissionSubject subject) {
return this.provider.createFunction(subject);
}
public @NonNull PermissionProvider getProvider() {
return this.provider;
}
/**
* Sets the {@link PermissionFunction} that should be used for the subject.
*
* <p>Specifying <code>null</code> will reset the provider to the default
* instance given when the event was posted.</p>
*
* @param provider the provider
*/
public void setProvider(@Nullable PermissionProvider provider) {
this.provider = provider == null ? this.defaultProvider : provider;
}
@Override
public String toString() {
return "PermissionsSetupEvent{" +
"subject=" + subject +
", defaultProvider=" + defaultProvider +
", provider=" + provider +
'}';
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides events to handle setting up permissions for permission subjects.
*/
package com.velocitypowered.api.event.permission;

Datei anzeigen

@ -0,0 +1,71 @@
package com.velocitypowered.api.event.player;
import com.velocitypowered.api.proxy.InboundConnection;
import org.checkerframework.checker.nullness.qual.Nullable;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.util.GameProfile;
/**
* This event is fired after the {@link com.velocitypowered.api.event.connection.PreLoginEvent} in order to set up the
* game profile for the user. This can be used to configure a custom profile for a user, i.e. skin replacement.
*/
public class GameProfileRequestEvent {
private final String username;
private final InboundConnection connection;
private final GameProfile originalProfile;
private final boolean onlineMode;
private GameProfile gameProfile;
public GameProfileRequestEvent(InboundConnection connection, GameProfile originalProfile, boolean onlineMode) {
this.connection = Preconditions.checkNotNull(connection, "connection");
this.originalProfile = Preconditions.checkNotNull(originalProfile, "originalProfile");
this.username = originalProfile.getName();
this.onlineMode = onlineMode;
}
public InboundConnection getConnection() {
return connection;
}
public String getUsername() {
return username;
}
public GameProfile getOriginalProfile() {
return originalProfile;
}
public boolean isOnlineMode() {
return onlineMode;
}
/**
* Returns the game profile that will be used to initialize the connection with. Should no profile be currently
* specified, the one generated by the proxy (for offline mode) or retrieved from the Mojang session servers (for
* online mode) will be returned instead.
* @return the user's {@link GameProfile}
*/
public GameProfile getGameProfile() {
return gameProfile == null ? originalProfile : gameProfile;
}
/**
* Sets the game profile to use for this connection. It is invalid to use this method on an online-mode connection.
* @param gameProfile the profile to use for the connection, {@code null} uses the original profile
*/
public void setGameProfile(@Nullable GameProfile gameProfile) {
Preconditions.checkState(!onlineMode, "Connection is in online mode, profiles can not be faked");
this.gameProfile = gameProfile;
}
@Override
public String toString() {
return "GameProfileRequestEvent{"+
"username=" + username +
", gameProfile=" + gameProfile +
"}";
}
}

Datei anzeigen

@ -0,0 +1,35 @@
package com.velocitypowered.api.event.player;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.ServerInfo;
/**
* This event is fired once the player has successfully connected to the target server and the connection to the previous
* server has been de-established.
*/
public class ServerConnectedEvent {
private final Player player;
private final ServerInfo server;
public ServerConnectedEvent(Player player, ServerInfo server) {
this.player = Preconditions.checkNotNull(player, "player");
this.server = Preconditions.checkNotNull(server, "server");
}
public Player getPlayer() {
return player;
}
public ServerInfo getServer() {
return server;
}
@Override
public String toString() {
return "ServerConnectedEvent{" +
"player=" + player +
", server=" + server +
'}';
}
}

Datei anzeigen

@ -0,0 +1,86 @@
package com.velocitypowered.api.event.player;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.ServerInfo;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.Optional;
/**
* This event is fired before the player connects to a server.
*/
public class ServerPreConnectEvent implements ResultedEvent<ServerPreConnectEvent.ServerResult> {
private final Player player;
private ServerResult result;
public ServerPreConnectEvent(Player player, ServerResult result) {
this.player = Preconditions.checkNotNull(player, "player");
this.result = Preconditions.checkNotNull(result, "result");
}
public Player getPlayer() {
return player;
}
@Override
public ServerResult getResult() {
return result;
}
@Override
public void setResult(@NonNull ServerResult result) {
this.result = Preconditions.checkNotNull(result, "result");
}
@Override
public String toString() {
return "ServerPreConnectEvent{" +
"player=" + player +
", result=" + result +
'}';
}
/**
* Represents the result of the {@link ServerPreConnectEvent}.
*/
public static class ServerResult implements ResultedEvent.Result {
private static final ServerResult DENIED = new ServerResult(false, null);
private final boolean allowed;
private final ServerInfo info;
private ServerResult(boolean allowed, @Nullable ServerInfo info) {
this.allowed = allowed;
this.info = info;
}
@Override
public boolean isAllowed() {
return allowed;
}
public Optional<ServerInfo> getInfo() {
return Optional.ofNullable(info);
}
@Override
public String toString() {
if (!allowed) {
return "denied";
}
return "allowed: connect to " + info.getName();
}
public static ServerResult denied() {
return DENIED;
}
public static ServerResult allowed(ServerInfo server) {
Preconditions.checkNotNull(server, "server");
return new ServerResult(true, server);
}
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides events for handling actions performed by players.
*/
package com.velocitypowered.api.event.player;

Datei anzeigen

@ -0,0 +1,11 @@
package com.velocitypowered.api.event.proxy;
/**
* This event is fired by the proxy after plugins have been loaded but before the proxy starts accepting connections.
*/
public class ProxyInitializeEvent {
@Override
public String toString() {
return "ProxyInitializeEvent";
}
}

Datei anzeigen

@ -0,0 +1,38 @@
package com.velocitypowered.api.event.proxy;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.server.ServerPing;
/**
* This event is fired when a server list ping request is sent by a remote client.
*/
public class ProxyPingEvent {
private final InboundConnection connection;
private ServerPing ping;
public ProxyPingEvent(InboundConnection connection, ServerPing ping) {
this.connection = Preconditions.checkNotNull(connection, "connection");
this.ping = Preconditions.checkNotNull(ping, "ping");
}
public InboundConnection getConnection() {
return connection;
}
public ServerPing getPing() {
return ping;
}
public void setPing(ServerPing ping) {
this.ping = Preconditions.checkNotNull(ping, "ping");
}
@Override
public String toString() {
return "ProxyPingEvent{" +
"connection=" + connection +
", ping=" + ping +
'}';
}
}

Datei anzeigen

@ -0,0 +1,12 @@
package com.velocitypowered.api.event.proxy;
/**
* This event is fired by the proxy after the proxy has stopped accepting connections but before the proxy process
* exits.
*/
public class ProxyShutdownEvent {
@Override
public String toString() {
return "ProxyShutdownEvent";
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides events for handling the lifecycle of the proxy.
*/
package com.velocitypowered.api.event.proxy;

Datei anzeigen

@ -1,5 +0,0 @@
package com.velocitypowered.api;
/**
* Welcome to the Velocity API documentation.
*/

Datei anzeigen

@ -0,0 +1,33 @@
package com.velocitypowered.api.permission;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Function that calculates the permission settings for a given
* {@link PermissionSubject}.
*/
@FunctionalInterface
public interface PermissionFunction {
/**
* A permission function that always returns {@link Tristate#TRUE}.
*/
PermissionFunction ALWAYS_TRUE = p -> Tristate.TRUE;
/**
* A permission function that always returns {@link Tristate#FALSE}.
*/
PermissionFunction ALWAYS_FALSE = p -> Tristate.FALSE;
/**
* A permission function that always returns {@link Tristate#UNDEFINED}.
*/
PermissionFunction ALWAYS_UNDEFINED = p -> Tristate.UNDEFINED;
/**
* Gets the subjects setting for a particular permission.
*
* @param permission the permission
* @return the value the permission is set to
*/
@NonNull Tristate getPermissionSetting(@NonNull String permission);
}

Datei anzeigen

@ -0,0 +1,17 @@
package com.velocitypowered.api.permission;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Provides {@link PermissionFunction}s for {@link PermissionSubject}s.
*/
@FunctionalInterface
public interface PermissionProvider {
/**
* Creates a {@link PermissionFunction} for the subject.
*
* @param subject the subject
* @return the function
*/
@NonNull PermissionFunction createFunction(@NonNull PermissionSubject subject);
}

Datei anzeigen

@ -0,0 +1,16 @@
package com.velocitypowered.api.permission;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Represents a object that has a set of queryable permissions.
*/
public interface PermissionSubject {
/**
* Determines whether or not the subject has a particular permission.
*
* @param permission the permission to check for
* @return whether or not the subject has the permission
*/
boolean hasPermission(@NonNull String permission);
}

Datei anzeigen

@ -0,0 +1,74 @@
package com.velocitypowered.api.permission;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Represents three different states of a setting.
*
* <p>Possible values:</p>
* <p></p>
* <ul>
* <li>{@link #TRUE} - a positive setting</li>
* <li>{@link #FALSE} - a negative (negated) setting</li>
* <li>{@link #UNDEFINED} - a non-existent setting</li>
* </ul>
*/
public enum Tristate {
/**
* A value indicating a positive setting
*/
TRUE(true),
/**
* A value indicating a negative (negated) setting
*/
FALSE(false),
/**
* A value indicating a non-existent setting
*/
UNDEFINED(false);
/**
* Returns a {@link Tristate} from a boolean
*
* @param val the boolean value
* @return {@link #TRUE} or {@link #FALSE}, if the value is <code>true</code> or <code>false</code>, respectively.
*/
public static @NonNull Tristate fromBoolean(boolean val) {
return val ? TRUE : FALSE;
}
/**
* Returns a {@link Tristate} from a nullable boolean.
*
* <p>Unlike {@link #fromBoolean(boolean)}, this method returns {@link #UNDEFINED}
* if the value is null.</p>
*
* @param val the boolean value
* @return {@link #UNDEFINED}, {@link #TRUE} or {@link #FALSE}, if the value
* is <code>null</code>, <code>true</code> or <code>false</code>, respectively.
*/
public static @NonNull Tristate fromNullableBoolean(@Nullable Boolean val) {
return val == null ? UNDEFINED : val ? TRUE : FALSE;
}
private final boolean booleanValue;
Tristate(boolean booleanValue) {
this.booleanValue = booleanValue;
}
/**
* Returns the value of the Tristate as a boolean.
*
* <p>A value of {@link #UNDEFINED} converts to false.</p>
*
* @return a boolean representation of the Tristate.
*/
public boolean asBoolean() {
return this.booleanValue;
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides the basic building blocks for a custom permission system.
*/
package com.velocitypowered.api.permission;

Datei anzeigen

@ -0,0 +1,30 @@
package com.velocitypowered.api.plugin;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Represents a dependency for a {@link Plugin}
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Dependency {
/**
* The plugin ID of the dependency.
*
* @return The dependency plugin ID
* @see Plugin#id()
*/
String id();
// TODO Add required version field
/**
* If this dependency is optional for the plugin to work. By default
* this is {@code false}.
*
* @return true if the dependency is optional for the plugin to work
*/
boolean optional() default false;
}

Datei anzeigen

@ -0,0 +1,19 @@
package com.velocitypowered.api.plugin;
public class InvalidPluginException extends Exception {
public InvalidPluginException() {
super();
}
public InvalidPluginException(String message) {
super(message);
}
public InvalidPluginException(String message, Throwable cause) {
super(message, cause);
}
public InvalidPluginException(Throwable cause) {
super(cause);
}
}

Datei anzeigen

@ -0,0 +1,66 @@
package com.velocitypowered.api.plugin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation used to describe a Velocity plugin.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Plugin {
/**
* The ID of the plugin. This ID should be unique as to
* not conflict with other plugins.
*
* The plugin ID must match the {@link PluginDescription#ID_PATTERN}.
*
* @return the ID for this plugin
*/
String id();
/**
* The human readable name of the plugin as to be used in descriptions and
* similar things.
*
* @return The plugin name, or an empty string if unknown
*/
String name() default "";
/**
* The version of the plugin.
*
* @return the version of the plugin, or an empty string if unknown
*/
String version() default "";
/**
* The description of the plugin, explaining what it can be used for.
*
* @return The plugin description, or an empty string if unknown
*/
String description() default "";
/**
* The URL or website of the plugin.
*
* @return The plugin url, or an empty string if unknown
*/
String url() default "";
/**
* The author of the plugin.
*
* @return the plugin's author, or empty if unknown
*/
String[] authors() default "";
/**
* The dependencies required to load before this plugin.
*
* @return the plugin dependencies
*/
Dependency[] dependencies() default {};
}

Datei anzeigen

@ -0,0 +1,26 @@
package com.velocitypowered.api.plugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Optional;
/**
* A wrapper around a plugin loaded by the proxy.
*/
public interface PluginContainer {
/**
* Returns the plugin's description.
*
* @return the plugin's description
*/
@NonNull PluginDescription getDescription();
/**
* Returns the created plugin if it is available.
*
* @return the instance if available
*/
default Optional<?> getInstance() {
return Optional.empty();
}
}

Datei anzeigen

@ -0,0 +1,106 @@
package com.velocitypowered.api.plugin;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.velocitypowered.api.plugin.meta.PluginDependency;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
/**
* Represents metadata for a specific version of a plugin.
*/
public interface PluginDescription {
/**
* The pattern plugin IDs must match. Plugin IDs may only contain
* alphanumeric characters, dashes or underscores, must start with
* an alphabetic character and cannot be longer than 64 characters.
*/
Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}");
/**
* Gets the qualified ID of the {@link Plugin} within this container.
*
* @return the plugin ID
* @see Plugin#id()
*/
String getId();
/**
* Gets the name of the {@link Plugin} within this container.
*
* @return an {@link Optional} with the plugin name, may be empty
* @see Plugin#name()
*/
default Optional<String> getName() {
return Optional.empty();
}
/**
* Gets the version of the {@link Plugin} within this container.
*
* @return an {@link Optional} with the plugin version, may be empty
* @see Plugin#version()
*/
default Optional<String> getVersion() {
return Optional.empty();
}
/**
* Gets the description of the {@link Plugin} within this container.
*
* @return an {@link Optional} with the plugin description, may be empty
* @see Plugin#description()
*/
default Optional<String> getDescription() {
return Optional.empty();
}
/**
* Gets the url or website of the {@link Plugin} within this container.
*
* @return an {@link Optional} with the plugin url, may be empty
* @see Plugin#url()
*/
default Optional<String> getUrl() {
return Optional.empty();
}
/**
* Gets the authors of the {@link Plugin} within this container.
*
* @return the plugin authors, may be empty
* @see Plugin#authors()
*/
default List<String> getAuthors() {
return ImmutableList.of();
}
/**
* Gets a {@link Collection} of all dependencies of the {@link Plugin} within
* this container.
*
* @return the plugin dependencies, can be empty
* @see Plugin#dependencies()
*/
default Collection<PluginDependency> getDependencies() {
return ImmutableSet.of();
}
default Optional<PluginDependency> getDependency(String id) {
return Optional.empty();
}
/**
* Returns the source the plugin was loaded from.
*
* @return the source the plugin was loaded from or {@link Optional#empty()}
* if unknown
*/
default Optional<Path> getSource() {
return Optional.empty();
}
}

Datei anzeigen

@ -0,0 +1,55 @@
package com.velocitypowered.api.plugin;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Optional;
import java.util.logging.Logger;
/**
* The class that manages plugins. This manager can retrieve
* {@link PluginContainer}s from {@link Plugin} instances, getting
* {@link Logger}s, etc.
*/
public interface PluginManager {
/**
* Gets the plugin container from an instance.
*
* @param instance the instance
* @return the container
*/
@NonNull Optional<PluginContainer> fromInstance(@NonNull Object instance);
/**
* Retrieves a {@link PluginContainer} based on its ID.
*
* @param id the plugin ID
* @return the plugin, if available
*/
@NonNull Optional<PluginContainer> getPlugin(@NonNull String id);
/**
* Gets a {@link Collection} of all {@link PluginContainer}s.
*
* @return the plugins
*/
@NonNull Collection<PluginContainer> getPlugins();
/**
* Checks if a plugin is loaded based on its ID.
*
* @param id the id of the {@link Plugin}
* @return {@code true} if loaded
*/
boolean isLoaded(@NonNull String id);
/**
* Adds the specified {@code path} to the plugin classpath.
*
* @param plugin the plugin
* @param path the path to the JAR you want to inject into the classpath
* @throws UnsupportedOperationException if the operation is not applicable to this plugin
*/
void addToClasspath(@NonNull Object plugin, @NonNull Path path);
}

Datei anzeigen

@ -0,0 +1,18 @@
package com.velocitypowered.api.plugin.annotation;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation requests that Velocity inject a {@link java.nio.file.Path} instance with a plugin-specific data
* directory.
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@BindingAnnotation
public @interface DataDirectory {
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides annotations to handle injecting dependencies for plugins.
*/
package com.velocitypowered.api.plugin.annotation;

Datei anzeigen

@ -0,0 +1,79 @@
package com.velocitypowered.api.plugin.meta;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.emptyToNull;
/**
* Represents a dependency on another plugin.
*/
public final class PluginDependency {
private final String id;
@Nullable private final String version;
private final boolean optional;
public PluginDependency(String id, @Nullable String version, boolean optional) {
this.id = checkNotNull(id, "id");
checkArgument(!id.isEmpty(), "id cannot be empty");
this.version = emptyToNull(version);
this.optional = optional;
}
/**
* Returns the plugin ID of this {@link PluginDependency}
*
* @return the plugin ID
*/
public String getId() {
return id;
}
/**
* Returns the version this {@link PluginDependency} should match.
*
* @return an {@link Optional} with the plugin version, may be empty
*/
public Optional<String> getVersion() {
return Optional.ofNullable(version);
}
/**
* Returns whether the dependency is optional for the plugin to work
* correctly.
*
* @return true if dependency is optional
*/
public boolean isOptional() {
return optional;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PluginDependency that = (PluginDependency) o;
return optional == that.optional &&
Objects.equals(id, that.id) &&
Objects.equals(version, that.version);
}
@Override
public int hashCode() {
return Objects.hash(id, version, optional);
}
@Override
public String toString() {
return "PluginDependency{" +
"id='" + id + '\'' +
", version='" + version + '\'' +
", optional=" + optional +
'}';
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides metadata for plugins.
*/
package com.velocitypowered.api.plugin.meta;

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides the Velocity plugin API.
*/
package com.velocitypowered.api.plugin;

Datei anzeigen

@ -1,28 +1,29 @@
package com.velocitypowered.api.proxy;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.api.proxy.server.ServerInfo;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/**
* Represents a connection request. A connection request is created using {@link Player#createConnectionRequest(ServerInfo)}
* and is used to allow a plugin to compose and request a connection to another Minecraft server using a fluent API.
* Provides a fluent interface to compose and send a connection request to another server behind the proxy. A connection
* request is created using {@link Player#createConnectionRequest(ServerInfo)}.
*/
public interface ConnectionRequestBuilder {
/**
* Returns the server that this connection request represents.
* @return the server this request will connect to
*/
ServerInfo getServer();
@NonNull ServerInfo getServer();
/**
* Initiates the connection to the remote server and emits a result on the {@link CompletableFuture} after the user
* has logged on. No messages will be communicated to the client: the user is responsible for all error handling.
* @return a {@link CompletableFuture} representing the status of this connection
*/
CompletableFuture<Result> connect();
@NonNull CompletableFuture<Result> connect();
/**
* Initiates the connection to the remote server without waiting for a result. Velocity will use generic error
@ -49,7 +50,7 @@ public interface ConnectionRequestBuilder {
Status getStatus();
/**
* Returns a reason for the failure to connect to the server. None may be provided.
* Returns an (optional) textual reason for the failure to connect to the server.
* @return the reason why the user could not connect to the server
*/
Optional<Component> getReason();

Datei anzeigen

@ -0,0 +1,33 @@
package com.velocitypowered.api.proxy;
import java.net.InetSocketAddress;
import java.util.Optional;
/**
* Represents an incoming connection to the proxy.
*/
public interface InboundConnection {
/**
* Returns the player's IP address.
* @return the player's IP
*/
InetSocketAddress getRemoteAddress();
/**
* Returns the hostname that the user entered into the client, if applicable.
* @return the hostname from the client
*/
Optional<InetSocketAddress> getVirtualHost();
/**
* Determine whether or not the player remains online.
* @return whether or not the player active
*/
boolean isActive();
/**
* Returns the current protocol version this connection uses.
* @return the protocol version the connection uses
*/
int getProtocolVersion();
}

Datei anzeigen

@ -1,18 +1,20 @@
package com.velocitypowered.api.proxy;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.util.MessagePosition;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import javax.annotation.Nonnull;
import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.UUID;
/**
* Represents a player who is connected to the proxy.
*/
public interface Player {
public interface Player extends CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink {
/**
* Returns the player's current username.
* @return the username
@ -29,25 +31,13 @@ public interface Player {
* Returns the server that the player is currently connected to.
* @return an {@link Optional} the server that the player is connected to, which may be empty
*/
Optional<ServerInfo> getCurrentServer();
/**
* Returns the player's IP address.
* @return the player's IP
*/
InetSocketAddress getRemoteAddress();
/**
* Determine whether or not the player remains online.
* @return whether or not the player active
*/
boolean isActive();
Optional<ServerConnection> getCurrentServer();
/**
* Sends a chat message to the player's client.
* @param component the chat message to send
*/
default void sendMessage(@Nonnull Component component) {
default void sendMessage(@NonNull Component component) {
sendMessage(component, MessagePosition.CHAT);
}
@ -56,12 +46,31 @@ public interface Player {
* @param component the chat message to send
* @param position the position for the message
*/
void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position);
void sendMessage(@NonNull Component component, @NonNull MessagePosition position);
/**
* Creates a new connection request so that the player can connect to another server.
* @param info the server to connect to
* @return a new connection request
*/
ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info);
ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info);
/**
* Sets the tab list header and footer for the player.
* @param header the header component
* @param footer the footer component
*/
void setHeaderAndFooter(Component header, Component footer);
/**
* Clears the tab list header and footer for the player.
*/
void clearHeaderAndFooter();
/**
* Disconnects the player with the specified reason. Once this method is called, further calls to other {@link Player}
* methods will become undefined.
* @param reason component with the reason
*/
void disconnect(Component reason);
}

Datei anzeigen

@ -0,0 +1,110 @@
package com.velocitypowered.api.proxy;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.proxy.messages.ChannelRegistrar;
import com.velocitypowered.api.scheduler.Scheduler;
import com.velocitypowered.api.proxy.server.ServerInfo;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
/**
* Provides an interface to a Minecraft server proxy.
*/
public interface ProxyServer {
/**
* Retrieves the player currently connected to this proxy by their Minecraft username. The search is case-insensitive.
* @param username the username to search for
* @return an {@link Optional} with the player, which may be empty
*/
Optional<Player> getPlayer(String username);
/**
* Retrieves the player currently connected to this proxy by their Minecraft UUID.
* @param uuid the UUID
* @return an {@link Optional} with the player, which may be empty
*/
Optional<Player> getPlayer(UUID uuid);
/**
* Retrieves all players currently connected to this proxy. This call may or may not be a snapshot of all players
* online.
* @return the players online on this proxy
*/
Collection<Player> getAllPlayers();
/**
* Returns the number of players currently connected to this proxy.
* @return the players on this proxy
*/
int getPlayerCount();
/**
* Retrieves a registered {@link ServerInfo} instance by its name. The search is case-insensitive.
* @param name the name of the server
* @return the registered server, which may be empty
*/
Optional<ServerInfo> getServerInfo(String name);
/**
* Retrieves all {@link ServerInfo}s registered with this proxy.
* @return the servers registered with this proxy
*/
Collection<ServerInfo> getAllServers();
/**
* Registers a server with this proxy. A server with this name should not already exist.
* @param server the server to register
*/
void registerServer(ServerInfo server);
/**
* Unregisters this server from the proxy.
* @param server the server to unregister
*/
void unregisterServer(ServerInfo server);
/**
* Returns an instance of {@link CommandSource} that can be used to determine if the command is being invoked by
* the console or a console-like executor. Plugins that execute commands are strongly urged to implement their own
* {@link CommandSource} instead of using the console invoker.
* @return the console command invoker
*/
CommandSource getConsoleCommandSource();
/**
* Gets the {@link PluginManager} instance.
*
* @return the plugin manager instance
*/
PluginManager getPluginManager();
/**
* Gets the {@link EventManager} instance.
*
* @return the event manager instance
*/
EventManager getEventManager();
/**
* Gets the {@link CommandManager} instance.
* @return the command manager
*/
CommandManager getCommandManager();
/**
* Gets the {@link Scheduler} instance.
* @return the scheduler instance
*/
Scheduler getScheduler();
/**
* Gets the {@link ChannelRegistrar} instance.
* @return the channel registrar
*/
ChannelRegistrar getChannelRegistrar();
}

Datei anzeigen

@ -0,0 +1,22 @@
package com.velocitypowered.api.proxy;
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
import com.velocitypowered.api.proxy.server.ServerInfo;
/**
* Represents a connection to a backend server from the proxy for a client.
*/
public interface ServerConnection extends ChannelMessageSource, ChannelMessageSink {
/**
* Returns the server that this connection is connected to.
* @return the server this connection is connected to
*/
ServerInfo getServerInfo();
/**
* Returns the player that this connection is associated with.
* @return the player for this connection
*/
Player getPlayer();
}

Datei anzeigen

@ -0,0 +1,12 @@
package com.velocitypowered.api.proxy.messages;
/**
* Represents a kind of channel identifier.
*/
public interface ChannelIdentifier {
/**
* Returns the textual representation of this identifier.
* @return the textual representation of the identifier
*/
String getId();
}

Datei anzeigen

@ -0,0 +1,13 @@
package com.velocitypowered.api.proxy.messages;
/**
* Represents something that can send plugin messages.
*/
public interface ChannelMessageSink {
/**
* Sends a plugin message to this target.
* @param identifier the channel identifier to send the message on
* @param data the data to send
*/
void sendPluginMessage(ChannelIdentifier identifier, byte[] data);
}

Datei anzeigen

@ -0,0 +1,7 @@
package com.velocitypowered.api.proxy.messages;
/**
* A marker interface that indicates a source of plugin messages.
*/
public interface ChannelMessageSource {
}

Datei anzeigen

@ -0,0 +1,20 @@
package com.velocitypowered.api.proxy.messages;
/**
* Represents an interface to register and unregister {@link MessageHandler} instances for handling plugin messages from
* the client or the server.
*/
public interface ChannelRegistrar {
/**
* Registers the specified message handler to listen for plugin messages on the specified channels.
* @param handler the handler to register
* @param identifiers the channel identifiers to register
*/
void register(MessageHandler handler, ChannelIdentifier... identifiers);
/**
* Unregisters the handler for the specified channel.
* @param identifiers the identifiers to unregister
*/
void unregister(ChannelIdentifier... identifiers);
}

Datei anzeigen

@ -0,0 +1,15 @@
package com.velocitypowered.api.proxy.messages;
/**
* Represents from "which side" of the proxy the plugin message came from.
*/
public enum ChannelSide {
/**
* The plugin message came from a server that a client was connected to.
*/
FROM_SERVER,
/**
* The plugin message came from the client.
*/
FROM_CLIENT
}

Datei anzeigen

@ -0,0 +1,48 @@
package com.velocitypowered.api.proxy.messages;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import java.util.Objects;
/**
* Reperesents a legacy channel identifier (for Minecraft 1.12 and below). For modern 1.13 plugin messages, please see
* {@link MinecraftChannelIdentifier}. This class is immutable and safe for multi-threaded use.
*/
public final class LegacyChannelIdentifier implements ChannelIdentifier {
private final String name;
public LegacyChannelIdentifier(String name) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "provided name is empty");
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "LegacyChannelIdentifier{" +
"name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LegacyChannelIdentifier that = (LegacyChannelIdentifier) o;
return Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public String getId() {
return name;
}
}

Datei anzeigen

@ -0,0 +1,28 @@
package com.velocitypowered.api.proxy.messages;
/**
* Represents a handler for handling plugin messages.
*/
public interface MessageHandler {
/**
* Handles an incoming plugin message.
* @param source the source of the plugin message
* @param side from where the plugin message originated
* @param identifier the channel on which the message was sent
* @param data the data inside the plugin message
* @return a {@link ForwardStatus} indicating whether or not to forward this plugin message on
*/
ForwardStatus handle(ChannelMessageSource source, ChannelSide side, ChannelIdentifier identifier, byte[] data);
enum ForwardStatus {
/**
* Forwards this plugin message on to the client or server, depending on the {@link ChannelSide} it originated
* from.
*/
FORWARD,
/**
* Discard the plugin message and do not forward it on.
*/
HANDLED
}
}

Datei anzeigen

@ -0,0 +1,81 @@
package com.velocitypowered.api.proxy.messages;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import java.util.Objects;
import java.util.regex.Pattern;
/**
* Represents a Minecraft 1.13+ channel identifier. This class is immutable and safe for multi-threaded use.
*/
public final class MinecraftChannelIdentifier implements ChannelIdentifier {
private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9\\-_]+");
private final String namespace;
private final String name;
private MinecraftChannelIdentifier(String namespace, String name) {
this.namespace = namespace;
this.name = name;
}
/**
* Creates an identifier in the default namespace ({@code minecraft}). Plugins are strongly encouraged to provide
* their own namespace.
* @param name the name in the default namespace to use
* @return a new channel identifier
*/
public static MinecraftChannelIdentifier forDefaultNamespace(String name) {
return new MinecraftChannelIdentifier("minecraft", name);
}
/**
* Creates an identifier in the specified namespace.
* @param namespace the namespace to use
* @param name the channel name inside the specified namespace
* @return a new channel identifier
*/
public static MinecraftChannelIdentifier create(String namespace, String name) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty");
Preconditions.checkArgument(!Strings.isNullOrEmpty(name), "namespace is null or empty");
Preconditions.checkArgument(VALID_IDENTIFIER_REGEX.matcher(namespace).matches(), "namespace is not valid");
Preconditions.checkArgument(VALID_IDENTIFIER_REGEX.matcher(name).matches(), "name is not valid");
return new MinecraftChannelIdentifier(namespace, name);
}
public String getNamespace() {
return namespace;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "MinecraftChannelIdentifier{" +
"namespace='" + namespace + '\'' +
", name='" + name + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MinecraftChannelIdentifier that = (MinecraftChannelIdentifier) o;
return Objects.equals(namespace, that.namespace) &&
Objects.equals(name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(namespace, name);
}
@Override
public String getId() {
return namespace + ":" + name;
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides an interface to receive, handle, and send plugin messages on the proxy from clients and servers.
*/
package com.velocitypowered.api.proxy.messages;

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides an interface to interact with the proxy at a low level.
*/
package com.velocitypowered.api.proxy;

Datei anzeigen

@ -1,6 +1,7 @@
package com.velocitypowered.api.server;
package com.velocitypowered.api.proxy.server;
import com.google.common.base.Preconditions;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.net.InetSocketAddress;
import java.util.Objects;
@ -9,24 +10,24 @@ import java.util.Objects;
* ServerInfo represents a server that a player can connect to. This object is immutable and safe for concurrent access.
*/
public final class ServerInfo {
private final String name;
private final InetSocketAddress address;
private final @NonNull String name;
private final @NonNull InetSocketAddress address;
/**
* Creates a new ServerInfo object.
* @param name the name for the server
* @param address the address of the server to connect to
*/
public ServerInfo(String name, InetSocketAddress address) {
public ServerInfo(@NonNull String name, @NonNull InetSocketAddress address) {
this.name = Preconditions.checkNotNull(name, "name");
this.address = Preconditions.checkNotNull(address, "address");
}
public final String getName() {
public final @NonNull String getName() {
return name;
}
public final InetSocketAddress getAddress() {
public final @NonNull InetSocketAddress getAddress() {
return address;
}

Datei anzeigen

@ -0,0 +1,244 @@
package com.velocitypowered.api.proxy.server;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.util.Favicon;
import net.kyori.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.*;
/**
* Represents a 1.7 and above server list ping response. This class is immutable.
*/
public class ServerPing {
private final Version version;
private final Players players;
private final Component description;
private final @Nullable Favicon favicon;
public ServerPing(@NonNull Version version, @NonNull Players players, @NonNull Component description, @Nullable Favicon favicon) {
this.version = Preconditions.checkNotNull(version, "version");
this.players = Preconditions.checkNotNull(players, "players");
this.description = Preconditions.checkNotNull(description, "description");
this.favicon = favicon;
}
public Version getVersion() {
return version;
}
public Players getPlayers() {
return players;
}
public Component getDescription() {
return description;
}
public Optional<Favicon> getFavicon() {
return Optional.ofNullable(favicon);
}
@Override
public String toString() {
return "ServerPing{" +
"version=" + version +
", players=" + players +
", description=" + description +
", favicon='" + favicon + '\'' +
'}';
}
public Builder asBuilder() {
Builder builder = new Builder();
builder.version = version;
builder.onlinePlayers = players.online;
builder.maximumPlayers = players.max;
builder.samplePlayers.addAll(players.sample);
builder.description = description;
builder.favicon = favicon;
return builder;
}
public static Builder builder() {
return new Builder();
}
/**
* A builder for {@link ServerPing} objects.
*/
public static class Builder {
private Version version;
private int onlinePlayers;
private int maximumPlayers;
private final List<SamplePlayer> samplePlayers = new ArrayList<>();
private Component description;
private Favicon favicon;
private Builder() {
}
public Builder version(Version version) {
this.version = Preconditions.checkNotNull(version, "version");
return this;
}
public Builder onlinePlayers(int onlinePlayers) {
this.onlinePlayers = onlinePlayers;
return this;
}
public Builder maximumPlayers(int maximumPlayers) {
this.maximumPlayers = maximumPlayers;
return this;
}
public Builder samplePlayers(SamplePlayer... players) {
this.samplePlayers.addAll(Arrays.asList(players));
return this;
}
public Builder clearSamplePlayers() {
this.samplePlayers.clear();
return this;
}
public Builder description(Component description) {
this.description = Preconditions.checkNotNull(description, "description");
return this;
}
public Builder favicon(Favicon favicon) {
this.favicon = Preconditions.checkNotNull(favicon, "favicon");
return this;
}
public ServerPing build() {
return new ServerPing(version, new Players(onlinePlayers, maximumPlayers, samplePlayers), description, favicon);
}
public Version getVersion() {
return version;
}
public int getOnlinePlayers() {
return onlinePlayers;
}
public int getMaximumPlayers() {
return maximumPlayers;
}
public List<SamplePlayer> getSamplePlayers() {
return samplePlayers;
}
public Component getDescription() {
return description;
}
public Favicon getFavicon() {
return favicon;
}
@Override
public String toString() {
return "Builder{" +
"version=" + version +
", onlinePlayers=" + onlinePlayers +
", maximumPlayers=" + maximumPlayers +
", samplePlayers=" + samplePlayers +
", description=" + description +
", favicon=" + favicon +
'}';
}
}
public static class Version {
private final int protocol;
private final String name;
public Version(int protocol, String name) {
this.protocol = protocol;
this.name = name;
}
public int getProtocol() {
return protocol;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Version{" +
"protocol=" + protocol +
", name='" + name + '\'' +
'}';
}
}
public static class Players {
private final int online;
private final int max;
private final List<SamplePlayer> sample;
public Players(int online, int max, List<SamplePlayer> sample) {
this.online = online;
this.max = max;
this.sample = ImmutableList.copyOf(sample);
}
public int getOnline() {
return online;
}
public int getMax() {
return max;
}
public List<SamplePlayer> getSample() {
return sample;
}
@Override
public String toString() {
return "Players{" +
"online=" + online +
", max=" + max +
", sample=" + sample +
'}';
}
}
public static class SamplePlayer {
private final String name;
private final UUID id;
public SamplePlayer(String name, UUID id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public UUID getId() {
return id;
}
@Override
public String toString() {
return "SamplePlayer{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides utilities to handle server information.
*/
package com.velocitypowered.api.proxy.server;

Datei anzeigen

@ -0,0 +1,24 @@
package com.velocitypowered.api.scheduler;
/**
* Represents a task that is scheduled to run on the proxy.
*/
public interface ScheduledTask {
/**
* Returns the plugin that scheduled this task.
* @return the plugin that scheduled this task
*/
Object plugin();
/**
* Returns the current status of this task.
* @return the current status of this task
*/
TaskStatus status();
/**
* Cancels this task. If the task is already running, the thread in which it is running will be interrupted.
* If the task is not currently running, Velocity will terminate it safely.
*/
void cancel();
}

Datei anzeigen

@ -0,0 +1,55 @@
package com.velocitypowered.api.scheduler;
import java.util.concurrent.TimeUnit;
/**
* Represents a scheduler to execute tasks on the proxy.
*/
public interface Scheduler {
/**
* Initializes a new {@link TaskBuilder} for creating a task on the proxy.
* @param plugin the plugin to request the task for
* @param runnable the task to run when scheduled
* @return the task builder
*/
TaskBuilder buildTask(Object plugin, Runnable runnable);
/**
* Represents a fluent interface to schedule tasks on the proxy.
*/
interface TaskBuilder {
/**
* Specifies that the task should delay its execution by the specified amount of time.
* @param time the time to delay by
* @param unit the unit of time for {@code time}
* @return this builder, for chaining
*/
TaskBuilder delay(int time, TimeUnit unit);
/**
* Specifies that the task should continue running after waiting for the specified amount, until it is cancelled.
* @param time the time to delay by
* @param unit the unit of time for {@code time}
* @return this builder, for chaining
*/
TaskBuilder repeat(int time, TimeUnit unit);
/**
* Clears the delay on this task.
* @return this builder, for chaining
*/
TaskBuilder clearDelay();
/**
* Clears the repeat interval on this task.
* @return this builder, for chaining
*/
TaskBuilder clearRepeat();
/**
* Schedules this task for execution.
* @return the scheduled task
*/
ScheduledTask schedule();
}
}

Datei anzeigen

@ -0,0 +1,16 @@
package com.velocitypowered.api.scheduler;
public enum TaskStatus {
/**
* The task is scheduled and is currently running.
*/
SCHEDULED,
/**
* The task was cancelled with {@link ScheduledTask#cancel()}.
*/
CANCELLED,
/**
* The task has run to completion. This is applicable only for tasks without a repeat.
*/
FINISHED
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides utilities for scheduling tasks with a fluent builder.
*/
package com.velocitypowered.api.scheduler;

Datei anzeigen

@ -0,0 +1,89 @@
package com.velocitypowered.api.util;
import com.google.common.base.Preconditions;
import org.checkerframework.checker.nullness.qual.NonNull;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Objects;
/**
* Represents a Minecraft server favicon. A Minecraft server favicon is a 64x64 image that can be displayed to a remote
* client that sends a Server List Ping packet, and is automatically displayed in the Minecraft client.
*/
public final class Favicon {
private final String base64Url;
/**
* Directly create a favicon using its Base64 URL directly. You are generally better served by the create() series
* of functions.
* @param base64Url the url for use with this favicon
*/
public Favicon(@NonNull String base64Url) {
this.base64Url = Preconditions.checkNotNull(base64Url, "base64Url");
}
/**
* Returns the Base64-encoded URI for this image.
* @return a URL representing this favicon
*/
public String getBase64Url() {
return base64Url;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Favicon favicon = (Favicon) o;
return Objects.equals(base64Url, favicon.base64Url);
}
@Override
public int hashCode() {
return Objects.hash(base64Url);
}
@Override
public String toString() {
return "Favicon{" +
"base64Url='" + base64Url + '\'' +
'}';
}
/**
* Creates a new {@code Favicon} from the specified {@code image}.
* @param image the image to use for the favicon
* @return the created {@link Favicon} instance
*/
public static Favicon create(@NonNull BufferedImage image) {
Preconditions.checkNotNull(image, "image");
Preconditions.checkArgument(image.getWidth() == 64 && image.getHeight() == 64, "Image does not have" +
" 64x64 dimensions (found %sx%s)", image.getWidth(), image.getHeight());
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
ImageIO.write(image, "PNG", os);
} catch (IOException e) {
throw new AssertionError(e);
}
return new Favicon("data:image/png;base64," + Base64.getEncoder().encodeToString(os.toByteArray()));
}
/**
* Creates a new {@code Favicon} by reading the image from the specified {@code path}.
* @param path the path to the image to create a favicon for
* @return the created {@link Favicon} instance
* @throws IOException if the file could not be read from the path
*/
public static Favicon create(@NonNull Path path) throws IOException {
try (InputStream stream = Files.newInputStream(path)) {
return create(ImageIO.read(stream));
}
}
}

Datei anzeigen

@ -1,20 +1,23 @@
package com.velocitypowered.proxy.data;
package com.velocitypowered.api.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.proxy.util.UuidUtils;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
import java.util.UUID;
public class GameProfile {
/**
* Represents a Mojang game profile. This class is immutable.
*/
public final class GameProfile {
private final String id;
private final String name;
private final List<Property> properties;
public GameProfile(String id, String name, List<Property> properties) {
this.id = id;
this.name = name;
public GameProfile(@NonNull String id, @NonNull String name, @NonNull List<Property> properties) {
this.id = Preconditions.checkNotNull(id, "id");
this.name = Preconditions.checkNotNull(name, "name");
this.properties = ImmutableList.copyOf(properties);
}
@ -31,10 +34,15 @@ public class GameProfile {
}
public List<Property> getProperties() {
return ImmutableList.copyOf(properties);
return properties;
}
public static GameProfile forOfflinePlayer(String username) {
/**
* Creates a game profile suitable for use in offline-mode.
* @param username the username to use
* @return the new offline-mode game profile
*/
public static GameProfile forOfflinePlayer(@NonNull String username) {
Preconditions.checkNotNull(username, "username");
String id = UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username));
return new GameProfile(id, username, ImmutableList.of());
@ -49,15 +57,15 @@ public class GameProfile {
'}';
}
public class Property {
public final class Property {
private final String name;
private final String value;
private final String signature;
public Property(String name, String value, String signature) {
this.name = name;
this.value = value;
this.signature = signature;
public Property(@NonNull String name, @NonNull String value, @NonNull String signature) {
this.name = Preconditions.checkNotNull(name, "name");
this.value = Preconditions.checkNotNull(value, "value");
this.signature = Preconditions.checkNotNull(signature, "signature");
}
public String getName() {

Datei anzeigen

@ -1,6 +1,7 @@
package com.velocitypowered.api.util;
import com.google.common.base.Preconditions;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.regex.Pattern;
@ -25,7 +26,7 @@ public class LegacyChatColorUtils {
* @param text the text to translate
* @return the translated text
*/
public static String translate(char originalChar, String text) {
public static String translate(char originalChar, @NonNull String text) {
Preconditions.checkNotNull(text, "text");
char[] textChars = text.toCharArray();
int foundSectionIdx = -1;
@ -58,7 +59,7 @@ public class LegacyChatColorUtils {
* @param text the text to remove color codes from
* @return a new String without Minecraft color codes
*/
public static String removeFormatting(String text) {
public static String removeFormatting(@NonNull String text) {
Preconditions.checkNotNull(text, "text");
return CHAT_COLOR_MATCHER.matcher(text).replaceAll("");
}

Datei anzeigen

@ -0,0 +1,50 @@
package com.velocitypowered.api.util;
import com.google.common.base.Preconditions;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.UUID;
/**
* Provides a small, useful selection of utilities for working with Minecraft UUIDs.
*/
public class UuidUtils {
private UuidUtils() {
throw new AssertionError();
}
/**
* Converts from an undashed Mojang-style UUID into a Java {@link UUID} object.
* @param string the string to convert
* @return the UUID object
*/
public static @NonNull UUID fromUndashed(final @NonNull String string) {
Objects.requireNonNull(string, "string");
Preconditions.checkArgument(string.length() == 32, "Length is incorrect");
return new UUID(
Long.parseUnsignedLong(string.substring(0, 16), 16),
Long.parseUnsignedLong(string.substring(16), 16)
);
}
/**
* Converts from a Java {@link UUID} object into an undashed Mojang-style UUID.
* @param uuid the UUID to convert
* @return the undashed UUID
*/
public static @NonNull String toUndashed(final @NonNull UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid");
return Long.toUnsignedString(uuid.getMostSignificantBits(), 16) + Long.toUnsignedString(uuid.getLeastSignificantBits(), 16);
}
/**
* Generates a UUID for use for offline mode.
* @param username the username to use
* @return the offline mode UUID
*/
public static @NonNull UUID generateOfflinePlayerUuid(@NonNull String username) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
}
}

Datei anzeigen

@ -0,0 +1,4 @@
/**
* Provides a selection of miscellaneous utilities for use by plugins and the proxy.
*/
package com.velocitypowered.api.util;

Datei anzeigen

@ -12,9 +12,21 @@ allprojects {
ext {
// dependency versions
junitVersion = '5.3.0-M1'
slf4jVersion = '1.7.25'
log4jVersion = '2.11.0'
nettyVersion = '4.1.28.Final'
guavaVersion = '25.1-jre'
getCurrentBranchName = {
new ByteArrayOutputStream().withStream { os ->
exec {
executable = "git"
args = ["rev-parse", "--abbrev-ref", "HEAD"]
standardOutput = os
}
return os.toString().trim()
}
}
}
repositories {
@ -30,4 +42,4 @@ allprojects {
junitXml.enabled = true
}
}
}
}

Datei anzeigen

@ -41,12 +41,12 @@ public class Natives {
public static final NativeCodeLoader<VelocityCompressorFactory> compressor = new NativeCodeLoader<>(
ImmutableList.of(
new NativeCodeLoader.Variant<>(NativeCodeLoader.MACOS,
copyAndLoadNative("/macosx/velocity-compress.dylib"), "native compression (macOS)",
copyAndLoadNative("/macosx/velocity-compress.dylib"), "native (macOS)",
NativeVelocityCompressor.FACTORY),
new NativeCodeLoader.Variant<>(NativeCodeLoader.LINUX,
copyAndLoadNative("/linux_x64/velocity-compress.so"), "native compression (Linux amd64)",
copyAndLoadNative("/linux_x64/velocity-compress.so"), "native (Linux amd64)",
NativeVelocityCompressor.FACTORY),
new NativeCodeLoader.Variant<>(NativeCodeLoader.ALWAYS, () -> {}, "Java compression", JavaVelocityCompressor.FACTORY)
new NativeCodeLoader.Variant<>(NativeCodeLoader.ALWAYS, () -> {}, "Java", JavaVelocityCompressor.FACTORY)
)
);

Datei anzeigen

@ -1,6 +1,7 @@
plugins {
id 'java'
id 'com.github.johnrengelman.shadow' version '2.0.4'
id 'de.sebastianboegl.shadow.transformer.log4j' version '2.1.1'
}
compileJava {
@ -21,17 +22,70 @@ jar {
dependencies {
compile project(':velocity-api')
compile project(':velocity-native')
compile "io.netty:netty-codec:${nettyVersion}"
compile "io.netty:netty-codec-http:${nettyVersion}"
compile "io.netty:netty-handler:${nettyVersion}"
compile "io.netty:netty-transport-native-epoll:${nettyVersion}"
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
compile "org.apache.logging.log4j:log4j-core:${log4jVersion}"
compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}"
compile 'net.minecrell:terminalconsoleappender:1.1.1'
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
compile 'it.unimi.dsi:fastutil:8.2.1'
compile 'net.kyori:event-method-asm:3.0.0'
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
}
shadowJar {
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/ints/*Int2*'
exclude 'it/unimi/dsi/fastutil/ints/IntAVL*'
exclude 'it/unimi/dsi/fastutil/ints/IntArray*'
exclude 'it/unimi/dsi/fastutil/ints/IntBi*'
exclude 'it/unimi/dsi/fastutil/ints/IntList*'
exclude 'it/unimi/dsi/fastutil/ints/IntOpen*'
exclude 'it/unimi/dsi/fastutil/ints/IntRB*'
exclude 'it/unimi/dsi/fastutil/ints/IntSet*'
exclude 'it/unimi/dsi/fastutil/ints/IntSorted*'
exclude 'it/unimi/dsi/fastutil/io/**'
exclude 'it/unimi/dsi/fastutil/longs/**'
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/*Object2IntLinked*'
exclude 'it/unimi/dsi/fastutil/objects/*Object*OpenCustom*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntRB*'
exclude 'it/unimi/dsi/fastutil/objects/*Object2IntSorted*'
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/*ObjectSorted*'
exclude 'it/unimi/dsi/fastutil/objects/*Reference*'
exclude 'it/unimi/dsi/fastutil/shorts/**'
}
artifacts {
archives shadowJar
}
}

Datei anzeigen

@ -1,12 +1,33 @@
package com.velocitypowered.proxy;
import com.velocitypowered.proxy.console.VelocityConsole;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.text.DecimalFormat;
public class Velocity {
public static void main(String... args) throws InterruptedException {
private static final Logger logger = LogManager.getLogger(Velocity.class);
private static long startTime;
static {
// We use BufferedImage for favicons, and on macOS this puts the Java application in the dock. How inconvenient.
// Force AWT to work with its head chopped off.
System.setProperty("java.awt.headless", "true");
}
public static void main(String... args) {
startTime = System.currentTimeMillis();
logger.info("Booting up Velocity...");
final VelocityServer server = VelocityServer.getServer();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown, "Shutdown thread"));
Thread.currentThread().join();
double bootTime = (System.currentTimeMillis() - startTime) / 1000d;
logger.info("Done ({}s)!", new DecimalFormat("#.##").format(bootTime));
new VelocityConsole(server).start();
}
}

Datei anzeigen

@ -1,17 +1,40 @@
package com.velocitypowered.proxy;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.network.ConnectionManager;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.event.EventManager;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.network.ConnectionManager;
import com.velocitypowered.proxy.command.ServerCommand;
import com.velocitypowered.proxy.command.ShutdownCommand;
import com.velocitypowered.proxy.command.VelocityCommand;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.command.VelocityCommandManager;
import com.velocitypowered.proxy.messages.VelocityChannelRegistrar;
import com.velocitypowered.proxy.plugin.VelocityEventManager;
import com.velocitypowered.proxy.protocol.util.FaviconSerializer;
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
import com.velocitypowered.proxy.scheduler.Sleeper;
import com.velocitypowered.proxy.scheduler.VelocityScheduler;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.Ratelimiter;
import com.velocitypowered.proxy.util.ServerMap;
import io.netty.bootstrap.Bootstrap;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.serializer.ComponentSerializers;
import net.kyori.text.serializer.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -22,13 +45,17 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
public class VelocityServer {
public class VelocityServer implements ProxyServer {
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
private static final VelocityServer INSTANCE = new VelocityServer();
public static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
.registerTypeHierarchyAdapter(Favicon.class, new FaviconSerializer())
.create();
private final ConnectionManager cm = new ConnectionManager();
@ -36,8 +63,33 @@ public class VelocityServer {
private NettyHttpClient httpClient;
private KeyPair serverKeyPair;
private final ServerMap servers = new ServerMap();
private final VelocityCommandManager commandManager = new VelocityCommandManager();
private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false);
private boolean shutdown = false;
private final VelocityPluginManager pluginManager = new VelocityPluginManager(this);
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private final CommandSource consoleCommandSource = new CommandSource() {
@Override
public void sendMessage(Component component) {
logger.info(ComponentSerializers.LEGACY.serialize(component));
}
@Override
public boolean hasPermission(String permission) {
return true;
}
};
private Ratelimiter ipAttemptLimiter;
private VelocityEventManager eventManager;
private VelocityScheduler scheduler;
private VelocityChannelRegistrar channelRegistrar;
private VelocityServer() {
commandManager.register(new VelocityCommand(), "velocity");
commandManager.register(new ServerCommand(), "server");
commandManager.register(new ShutdownCommand(), "shutdown");
}
public static VelocityServer getServer() {
@ -52,12 +104,12 @@ public class VelocityServer {
return configuration;
}
public void start() {
logger.info("Using {}", Natives.compressor.getLoadedVariant());
logger.info("Using {}", Natives.cipher.getLoadedVariant());
@Override
public VelocityCommandManager getCommandManager() {
return commandManager;
}
// Create a key pair
logger.info("Booting up Velocity...");
public void start() {
try {
Path configPath = Paths.get("velocity.toml");
try {
@ -82,10 +134,55 @@ public class VelocityServer {
}
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
ipAttemptLimiter = new Ratelimiter(configuration.getLoginRatelimit());
httpClient = new NettyHttpClient(this);
eventManager = new VelocityEventManager(pluginManager);
scheduler = new VelocityScheduler(pluginManager, Sleeper.SYSTEM);
channelRegistrar = new VelocityChannelRegistrar();
loadPlugins();
try {
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
// to fully initialize before we accept any connections to the server.
eventManager.fire(new ProxyInitializeEvent()).get();
} catch (InterruptedException | ExecutionException e) {
// Ignore, we don't care. InterruptedException is unlikely to happen (and if it does, you've got bigger
// issues) and there is almost no chance ExecutionException will be thrown.
}
this.cm.bind(configuration.getBind());
if (configuration.isQueryEnabled()) {
this.cm.queryBind(configuration.getBind().getHostString(), configuration.getQueryPort());
}
}
private void loadPlugins() {
logger.info("Loading plugins...");
try {
Path pluginPath = Paths.get("plugins");
if (Files.notExists(pluginPath)) {
Files.createDirectory(pluginPath);
} else {
if (!Files.isDirectory(pluginPath)) {
logger.warn("Plugin location {} is not a directory, continuing without loading plugins", pluginPath);
return;
}
pluginManager.loadPlugins(pluginPath);
}
} catch (Exception e) {
logger.error("Couldn't load plugins", e);
}
// Register the plugin main classes so that we may proceed with firing the proxy initialize event
pluginManager.getPlugins().forEach(container -> {
container.getInstance().ifPresent(plugin -> eventManager.register(plugin, plugin));
});
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
}
public ServerMap getServers() {
@ -96,11 +193,122 @@ public class VelocityServer {
return this.cm.createWorker();
}
public boolean isShutdown() {
return shutdown;
}
public void shutdown() {
if (!shutdownInProgress.compareAndSet(false, true)) return;
logger.info("Shutting down the proxy...");
for (ConnectedPlayer player : ImmutableList.copyOf(connectionsByUuid.values())) {
player.close(TextComponent.of("Proxy shutting down."));
}
this.cm.shutdown();
eventManager.fire(new ProxyShutdownEvent());
try {
if (!eventManager.shutdown() || !scheduler.shutdown()) {
logger.error("Your plugins took over 10 seconds to shut down.");
}
} catch (InterruptedException e) {
// Not much we can do about this...
}
shutdown = true;
}
public NettyHttpClient getHttpClient() {
return httpClient;
}
public Ratelimiter getIpAttemptLimiter() {
return ipAttemptLimiter;
}
public boolean registerConnection(ConnectedPlayer connection) {
String lowerName = connection.getUsername().toLowerCase(Locale.US);
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
return false;
}
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
connectionsByName.remove(lowerName, connection);
return false;
}
return true;
}
public void unregisterConnection(ConnectedPlayer connection) {
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
connectionsByUuid.remove(connection.getUniqueId(), connection);
}
@Override
public Optional<Player> getPlayer(String username) {
Preconditions.checkNotNull(username, "username");
return Optional.ofNullable(connectionsByName.get(username.toLowerCase(Locale.US)));
}
@Override
public Optional<Player> getPlayer(UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid");
return Optional.ofNullable(connectionsByUuid.get(uuid));
}
@Override
public Collection<Player> getAllPlayers() {
return ImmutableList.copyOf(connectionsByUuid.values());
}
@Override
public int getPlayerCount() {
return connectionsByUuid.size();
}
@Override
public Optional<ServerInfo> getServerInfo(String name) {
Preconditions.checkNotNull(name, "name");
return servers.getServer(name);
}
@Override
public Collection<ServerInfo> getAllServers() {
return servers.getAllServers();
}
@Override
public void registerServer(ServerInfo server) {
servers.register(server);
}
@Override
public void unregisterServer(ServerInfo server) {
servers.unregister(server);
}
@Override
public CommandSource getConsoleCommandSource() {
return consoleCommandSource;
}
@Override
public PluginManager getPluginManager() {
return pluginManager;
}
@Override
public EventManager getEventManager() {
return eventManager;
}
@Override
public VelocityScheduler getScheduler() {
return scheduler;
}
@Override
public VelocityChannelRegistrar getChannelRegistrar() {
return channelRegistrar;
}
}

Datei anzeigen

@ -0,0 +1,58 @@
package com.velocitypowered.proxy.command;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class ServerCommand implements Command {
@Override
public void execute(CommandSource source, String[] args) {
if (!(source instanceof Player)) {
source.sendMessage(TextComponent.of("Only players may run this command.", TextColor.RED));
return;
}
Player player = (Player) source;
if (args.length == 1) {
// Trying to connect to a server.
String serverName = args[0];
Optional<ServerInfo> server = VelocityServer.getServer().getServerInfo(serverName);
if (!server.isPresent()) {
player.sendMessage(TextComponent.of("Server " + serverName + " doesn't exist.", TextColor.RED));
return;
}
player.createConnectionRequest(server.get()).fireAndForget();
} else {
String serverList = VelocityServer.getServer().getAllServers().stream()
.map(ServerInfo::getName)
.collect(Collectors.joining(", "));
player.sendMessage(TextComponent.of("Available servers: " + serverList, TextColor.YELLOW));
}
}
@Override
public List<String> suggest(CommandSource source, String[] currentArgs) {
if (currentArgs.length == 0) {
return VelocityServer.getServer().getAllServers().stream()
.map(ServerInfo::getName)
.collect(Collectors.toList());
} else if (currentArgs.length == 1) {
return VelocityServer.getServer().getAllServers().stream()
.map(ServerInfo::getName)
.filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length()))
.collect(Collectors.toList());
} else {
return ImmutableList.of();
}
}
}

Datei anzeigen

@ -0,0 +1,18 @@
package com.velocitypowered.proxy.command;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
public class ShutdownCommand implements Command {
@Override
public void execute(CommandSource source, String[] args) {
if (source != VelocityServer.getServer().getConsoleCommandSource()) {
source.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED));
return;
}
VelocityServer.getServer().shutdown();
}
}

Datei anzeigen

@ -0,0 +1,40 @@
package com.velocitypowered.proxy.command;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.event.ClickEvent;
import net.kyori.text.format.TextColor;
public class VelocityCommand implements Command {
@Override
public void execute(CommandSource source, String[] args) {
String implVersion = VelocityServer.class.getPackage().getImplementationVersion();
TextComponent thisIsVelocity = TextComponent.builder()
.content("This is ")
.append(TextComponent.of("Velocity " + implVersion, TextColor.DARK_AQUA))
.append(TextComponent.of(", the next generation Minecraft: Java Edition proxy.").resetStyle())
.build();
TextComponent velocityInfo = TextComponent.builder()
.content("Copyright 2018 Velocity Contributors. Velocity is freely licensed under the terms of the " +
"MIT License.")
.build();
TextComponent velocityWebsite = TextComponent.builder()
.content("Visit the ")
.append(TextComponent.builder("Velocity website")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.velocitypowered.com"))
.build())
.append(TextComponent.of(" or the ").resetStyle())
.append(TextComponent.builder("Velocity GitHub")
.color(TextColor.GREEN)
.clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/astei/velocity"))
.build())
.build();
source.sendMessage(thisIsVelocity);
source.sendMessage(velocityInfo);
source.sendMessage(velocityWebsite);
}
}

Datei anzeigen

@ -0,0 +1,85 @@
package com.velocitypowered.proxy.command;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.CommandManager;
import java.util.*;
import java.util.stream.Collectors;
public class VelocityCommandManager implements CommandManager {
private final Map<String, Command> commands = new HashMap<>();
@Override
public void register(final Command command, final String... aliases) {
Preconditions.checkNotNull(aliases, "aliases");
Preconditions.checkNotNull(command, "executor");
for (int i = 0, length = aliases.length; i < length; i++) {
final String alias = aliases[i];
Preconditions.checkNotNull(aliases, "alias at index %s", i);
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
}
}
@Override
public void unregister(final String alias) {
Preconditions.checkNotNull(alias, "name");
this.commands.remove(alias.toLowerCase(Locale.ENGLISH));
}
@Override
public boolean execute(CommandSource source, String cmdLine) {
Preconditions.checkNotNull(source, "invoker");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return false;
}
String alias = split[0];
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
if (command == null) {
return false;
}
try {
command.execute(source, actualArgs);
return true;
} catch (Exception e) {
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
}
}
public Optional<List<String>> offerSuggestions(CommandSource source, String cmdLine) {
Preconditions.checkNotNull(source, "source");
Preconditions.checkNotNull(cmdLine, "cmdLine");
String[] split = cmdLine.split(" ", -1);
if (split.length == 0) {
return Optional.empty();
}
String command = split[0];
if (split.length == 1) {
return Optional.of(commands.keySet().stream()
.filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length()))
.collect(Collectors.toList()));
}
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
Command executor = commands.get(command);
if (executor == null) {
return Optional.empty();
}
try {
return Optional.of(executor.suggest(source, actualArgs));
} catch (Exception e) {
throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + source, e);
}
}
}

Datei anzeigen

@ -1,6 +1,6 @@
package com.velocitypowered.proxy.config;
public enum IPForwardingMode {
public enum PlayerInfoForwarding {
NONE,
LEGACY,
MODERN

Datei anzeigen

@ -2,8 +2,10 @@ package com.velocitypowered.proxy.config;
import com.google.common.collect.ImmutableMap;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.api.util.LegacyChatColorUtils;
import io.netty.buffer.ByteBufUtil;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import org.apache.logging.log4j.LogManager;
@ -15,6 +17,7 @@ import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -26,27 +29,39 @@ public class VelocityConfiguration {
private final String motd;
private final int showMaxPlayers;
private final boolean onlineMode;
private final IPForwardingMode ipForwardingMode;
private final PlayerInfoForwarding playerInfoForwardingMode;
private final Map<String, String> servers;
private final List<String> attemptConnectionOrder;
private final int compressionThreshold;
private final int compressionLevel;
private final int loginRatelimit;
private final boolean queryEnabled;
private final int queryPort;
private Component motdAsComponent;
private Favicon favicon;
private final byte[] forwardingSecret;
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
IPForwardingMode ipForwardingMode, Map<String, String> servers,
PlayerInfoForwarding playerInfoForwardingMode, Map<String, String> servers,
List<String> attemptConnectionOrder, int compressionThreshold,
int compressionLevel) {
int compressionLevel, int loginRatelimit, boolean queryEnabled,
int queryPort, byte[] forwardingSecret) {
this.bind = bind;
this.motd = motd;
this.showMaxPlayers = showMaxPlayers;
this.onlineMode = onlineMode;
this.ipForwardingMode = ipForwardingMode;
this.playerInfoForwardingMode = playerInfoForwardingMode;
this.servers = servers;
this.attemptConnectionOrder = attemptConnectionOrder;
this.compressionThreshold = compressionThreshold;
this.compressionLevel = compressionLevel;
this.loginRatelimit = loginRatelimit;
this.queryEnabled = queryEnabled;
this.queryPort = queryPort;
this.forwardingSecret = forwardingSecret;
}
public boolean validate() {
@ -68,9 +83,15 @@ public class VelocityConfiguration {
logger.info("Proxy is running in offline mode!");
}
switch (ipForwardingMode) {
switch (playerInfoForwardingMode) {
case NONE:
logger.info("IP forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
logger.info("Player info forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
break;
case MODERN:
if (forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set.");
valid = false;
}
break;
}
@ -109,23 +130,51 @@ public class VelocityConfiguration {
if (compressionLevel < -1 || compressionLevel > 9) {
logger.error("Invalid compression level {}", compressionLevel);
valid = false;
} else if (compressionLevel == 0) {
logger.warn("ALL packets going through the proxy are going to be uncompressed. This will increase bandwidth usage.");
}
if (compressionThreshold < -1) {
logger.error("Invalid compression threshold {}", compressionLevel);
valid = false;
} else if (compressionThreshold == 0) {
logger.warn("ALL packets going through the proxy are going to be compressed. This may hurt performance.");
}
if (loginRatelimit < 0) {
logger.error("Invalid login ratelimit {}", loginRatelimit);
valid = false;
}
loadFavicon();
return valid;
}
private void loadFavicon() {
Path faviconPath = Paths.get("server-icon.png");
if (Files.exists(faviconPath)) {
try {
this.favicon = Favicon.create(faviconPath);
} catch (Exception e) {
logger.info("Unable to load your server-icon.png, continuing without it.", e);
}
}
}
public InetSocketAddress getBind() {
return AddressUtil.parseAddress(bind);
}
public boolean isQueryEnabled() {
return queryEnabled;
}
public int getQueryPort() {
return queryPort;
}
public String getMotd() {
return motd;
}
@ -149,8 +198,8 @@ public class VelocityConfiguration {
return onlineMode;
}
public IPForwardingMode getIpForwardingMode() {
return ipForwardingMode;
public PlayerInfoForwarding getPlayerInfoForwardingMode() {
return playerInfoForwardingMode;
}
public Map<String, String> getServers() {
@ -169,6 +218,18 @@ public class VelocityConfiguration {
return compressionLevel;
}
public int getLoginRatelimit() {
return loginRatelimit;
}
public Favicon getFavicon() {
return favicon;
}
public byte[] getForwardingSecret() {
return forwardingSecret;
}
@Override
public String toString() {
return "VelocityConfiguration{" +
@ -176,12 +237,17 @@ public class VelocityConfiguration {
", motd='" + motd + '\'' +
", showMaxPlayers=" + showMaxPlayers +
", onlineMode=" + onlineMode +
", ipForwardingMode=" + ipForwardingMode +
", playerInfoForwardingMode=" + playerInfoForwardingMode +
", servers=" + servers +
", attemptConnectionOrder=" + attemptConnectionOrder +
", compressionThreshold=" + compressionThreshold +
", compressionLevel=" + compressionLevel +
", loginRatelimit=" + loginRatelimit +
", queryEnabled=" + queryEnabled +
", queryPort=" + queryPort +
", motdAsComponent=" + motdAsComponent +
", favicon=" + favicon +
", forwardingSecret=" + ByteBufUtil.hexDump(forwardingSecret) +
'}';
}
@ -200,16 +266,23 @@ public class VelocityConfiguration {
}
}
byte[] forwardingSecret = toml.getString("player-info-forwarding-secret", "5up3r53cr3t")
.getBytes(StandardCharsets.UTF_8);
return new VelocityConfiguration(
toml.getString("bind"),
toml.getString("motd"),
toml.getLong("show-max-players").intValue(),
toml.getBoolean("online-mode"),
IPForwardingMode.valueOf(toml.getString("ip-forwarding").toUpperCase()),
toml.getString("bind", "0.0.0.0:25577"),
toml.getString("motd", "&3A Velocity Server"),
toml.getLong("show-max-players", 500L).intValue(),
toml.getBoolean("online-mode", true),
PlayerInfoForwarding.valueOf(toml.getString("player-info-forwarding", "MODERN").toUpperCase()),
ImmutableMap.copyOf(servers),
toml.getTable("servers").getList("try"),
toml.getTable("advanced").getLong("compression-threshold", 1024L).intValue(),
toml.getTable("advanced").getLong("compression-level", -1L).intValue());
toml.getTable("advanced").getLong("compression-level", -1L).intValue(),
toml.getTable("advanced").getLong("login-ratelimit", 3000L).intValue(),
toml.getTable("query").getBoolean("enabled", false),
toml.getTable("query").getLong("port", 25577L).intValue(),
forwardingSecret);
}
}
}

Datei anzeigen

@ -5,11 +5,12 @@ import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.natives.encryption.VelocityCipherFactory;
import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.protocol.PacketWrapper;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.natives.encryption.JavaVelocityCipher;
import com.velocitypowered.natives.encryption.VelocityCipher;
import com.velocitypowered.proxy.protocol.netty.*;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
@ -23,14 +24,14 @@ import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import static com.velocitypowered.network.Connections.CIPHER_DECODER;
import static com.velocitypowered.network.Connections.CIPHER_ENCODER;
import static com.velocitypowered.network.Connections.COMPRESSION_DECODER;
import static com.velocitypowered.network.Connections.COMPRESSION_ENCODER;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.proxy.network.Connections.CIPHER_DECODER;
import static com.velocitypowered.proxy.network.Connections.CIPHER_ENCODER;
import static com.velocitypowered.proxy.network.Connections.COMPRESSION_DECODER;
import static com.velocitypowered.proxy.network.Connections.COMPRESSION_ENCODER;
import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER;
import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
/**
* A utility class to make working with the pipeline a little less painful and transparently handles certain Minecraft
@ -40,7 +41,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
private final Channel channel;
private boolean closed;
private StateRegistry state;
private MinecraftSessionHandler sessionHandler;
private int protocolVersion;
@ -48,7 +48,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public MinecraftConnection(Channel channel) {
this.channel = channel;
this.closed = false;
this.state = StateRegistry.HANDSHAKE;
}
@ -72,24 +71,17 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (association != null) {
logger.info("{} has disconnected", association);
}
teardown();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof PacketWrapper) {
PacketWrapper pw = (PacketWrapper) msg;
if (msg instanceof MinecraftPacket) {
sessionHandler.handle((MinecraftPacket) msg);
} else if (msg instanceof ByteBuf) {
try {
if (sessionHandler != null) {
if (pw.getPacket() == null) {
sessionHandler.handleUnknown(pw.getBuffer());
} else {
sessionHandler.handle(pw.getPacket());
}
}
sessionHandler.handleUnknown((ByteBuf) msg);
} finally {
ReferenceCountUtil.release(pw.getBuffer());
ReferenceCountUtil.release(msg);
}
}
}
@ -107,40 +99,38 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
logger.error("{} encountered an exception", ctx.channel().remoteAddress(), cause);
}
closed = true;
ctx.close();
}
}
public void write(Object msg) {
ensureOpen();
channel.writeAndFlush(msg, channel.voidPromise());
if (channel.isActive()) {
channel.writeAndFlush(msg, channel.voidPromise());
}
}
public void delayedWrite(Object msg) {
ensureOpen();
channel.write(msg, channel.voidPromise());
if (channel.isActive()) {
channel.write(msg, channel.voidPromise());
}
}
public void flush() {
ensureOpen();
channel.flush();
if (channel.isActive()) {
channel.flush();
}
}
public void closeWith(Object msg) {
ensureOpen();
teardown();
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
if (channel.isActive()) {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
public void close() {
ensureOpen();
teardown();
channel.close();
}
public void teardown() {
closed = true;
if (channel.isActive()) {
channel.close();
}
}
public Channel getChannel() {
@ -148,7 +138,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
public boolean isClosed() {
return closed;
return !channel.isActive();
}
public StateRegistry getState() {
@ -167,8 +157,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion);
this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion);
if (protocolVersion != ProtocolConstants.LEGACY) {
this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion);
this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion);
} else {
// Legacy handshake handling
this.channel.pipeline().remove(MINECRAFT_ENCODER);
this.channel.pipeline().remove(MINECRAFT_DECODER);
}
}
public MinecraftSessionHandler getSessionHandler() {
@ -184,10 +180,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
private void ensureOpen() {
Preconditions.checkState(!closed, "Connection is closed.");
Preconditions.checkState(!isClosed(), "Connection is closed.");
}
public void setCompressionThreshold(int threshold) {
ensureOpen();
if (threshold == -1) {
channel.pipeline().remove(COMPRESSION_DECODER);
channel.pipeline().remove(COMPRESSION_ENCODER);
@ -204,6 +202,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
}
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
ensureOpen();
SecretKey key = new SecretKeySpec(secret, "AES");
VelocityCipherFactory factory = Natives.cipher.get();

Datei anzeigen

@ -4,7 +4,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import io.netty.buffer.ByteBuf;
public interface MinecraftSessionHandler {
void handle(MinecraftPacket packet) throws Exception;
void handle(MinecraftPacket packet);
default void handleUnknown(ByteBuf buf) {
// No-op: we'll release the buffer later.

Datei anzeigen

@ -1,5 +1,9 @@
package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.proxy.messages.ChannelSide;
import com.velocitypowered.api.proxy.messages.MessageHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
@ -7,31 +11,40 @@ import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
private final ServerConnection connection;
private final VelocityServerConnection connection;
public BackendPlaySessionHandler(ServerConnection connection) {
public BackendPlaySessionHandler(VelocityServerConnection connection) {
this.connection = connection;
}
@Override
public void activated() {
VelocityServer.getServer().getEventManager().fireAndForget(new ServerConnectedEvent(connection.getPlayer(),
connection.getServerInfo()));
}
@Override
public void handle(MinecraftPacket packet) {
if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors.
connection.getMinecraftConnection().close();
return;
}
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
if (packet instanceof KeepAlive) {
// Forward onto the server
connection.getMinecraftConnection().write(packet);
// Forward onto the player
playerHandler.setLastPing(((KeepAlive) packet).getRandomId());
connection.getPlayer().getConnection().write(packet);
} else if (packet instanceof Disconnect) {
Disconnect original = (Disconnect) packet;
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), original);
connection.getPlayer().handleConnectionException(connection.getServerInfo(), original);
} else if (packet instanceof JoinGame) {
playerHandler.handleBackendJoinGame((JoinGame) packet);
} else if (packet instanceof Respawn) {
// Record the dimension switch, and then forward the packet on.
playerHandler.setCurrentDimension(((Respawn) packet).getDimension());
connection.getProxyPlayer().getConnection().write(packet);
} else if (packet instanceof BossBar) {
BossBar bossBar = (BossBar) packet;
switch (bossBar.getAction()) {
@ -42,7 +55,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
playerHandler.getServerBossBars().remove(bossBar.getUuid());
break;
}
connection.getProxyPlayer().getConnection().write(packet);
connection.getPlayer().getConnection().write(packet);
} else if (packet instanceof PluginMessage) {
PluginMessage pm = (PluginMessage) packet;
if (!canForwardPluginMessage(pm)) {
@ -50,45 +63,52 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
}
if (PluginMessageUtil.isMCBrand(pm)) {
connection.getProxyPlayer().getConnection().write(PluginMessageUtil.rewriteMCBrand(pm));
connection.getPlayer().getConnection().write(PluginMessageUtil.rewriteMCBrand(pm));
return;
}
connection.getProxyPlayer().getConnection().write(pm);
MessageHandler.ForwardStatus status = VelocityServer.getServer().getChannelRegistrar().handlePluginMessage(
connection, ChannelSide.FROM_SERVER, pm);
if (status == MessageHandler.ForwardStatus.FORWARD) {
connection.getPlayer().getConnection().write(pm);
}
} else {
// Just forward the packet on. We don't have anything to handle at this time.
if (packet instanceof ScoreboardTeam ||
packet instanceof ScoreboardObjective ||
packet instanceof ScoreboardSetScore ||
packet instanceof ScoreboardDisplay) {
playerHandler.handleServerScoreboardPacket(packet);
}
connection.getProxyPlayer().getConnection().write(packet);
connection.getPlayer().getConnection().write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
if (!connection.getPlayer().isActive()) {
// Connection was left open accidentally. Close it so as to avoid "You logged in from another location"
// errors.
connection.getMinecraftConnection().close();
return;
}
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
ByteBuf remapped = playerHandler.getIdRemapper().remap(buf, ProtocolConstants.Direction.CLIENTBOUND);
connection.getProxyPlayer().getConnection().write(remapped);
connection.getPlayer().getConnection().write(remapped);
}
@Override
public void exception(Throwable throwable) {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), throwable);
connection.getPlayer().handleConnectionException(connection.getServerInfo(), throwable);
}
private boolean canForwardPluginMessage(PluginMessage message) {
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
(ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler();
if (connection.getMinecraftConnection().getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) {
return message.getChannel().startsWith("MC|") ||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel());
playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
VelocityServer.getServer().getChannelRegistrar().registered(message.getChannel());
} else {
return message.getChannel().startsWith("minecraft:") ||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel());
playerHandler.getClientPluginMsgChannels().contains(message.getChannel()) ||
VelocityServer.getServer().getChannelRegistrar().registered(message.getChannel());
}
}
}

Datei anzeigen

@ -2,11 +2,12 @@ package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.IPForwardingMode;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
@ -14,49 +15,40 @@ import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelPipeline;
import net.kyori.text.TextComponent;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CompletableFuture;
public class LoginSessionHandler implements MinecraftSessionHandler {
private final ServerConnection connection;
private ScheduledFuture<?> forwardingCheckTask;
private final VelocityServerConnection connection;
private boolean informationForwarded;
public LoginSessionHandler(ServerConnection connection) {
public LoginSessionHandler(VelocityServerConnection connection) {
this.connection = connection;
}
@Override
public void activated() {
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN) {
forwardingCheckTask = connection.getMinecraftConnection().getChannel().eventLoop().schedule(() -> {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(),
TextComponent.of("Your server did not send the forwarding request in time. Is it set up correctly?"));
}, 1, TimeUnit.SECONDS);
}
}
@Override
public void handle(MinecraftPacket packet) {
if (packet instanceof EncryptionRequest) {
throw new IllegalStateException("Backend server is online-mode!");
} else if (packet instanceof LoginPluginMessage) {
LoginPluginMessage message = (LoginPluginMessage) packet;
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN &&
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN &&
message.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(true);
response.setId(message.getId());
response.setData(createForwardingData(connection.getProxyPlayer().getRemoteAddress().getHostString(),
connection.getProxyPlayer().getProfile()));
response.setData(createForwardingData(configuration.getForwardingSecret(),
connection.getPlayer().getRemoteAddress().getHostString(),
connection.getPlayer().getProfile()));
connection.getMinecraftConnection().write(response);
cancelForwardingCheck();
ServerLogin login = new ServerLogin();
login.setUsername(connection.getProxyPlayer().getUsername());
connection.getMinecraftConnection().write(login);
informationForwarded = true;
} else {
// Don't understand
LoginPluginResponse response = new LoginPluginResponse();
@ -67,78 +59,92 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
} else if (packet instanceof Disconnect) {
Disconnect disconnect = (Disconnect) packet;
connection.disconnect();
// Do we have an outstanding notification? If so, fulfill it.
doNotify(ConnectionRequestResults.forDisconnect(disconnect));
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), disconnect);
connection.disconnect();
} else if (packet instanceof SetCompression) {
SetCompression sc = (SetCompression) packet;
connection.getMinecraftConnection().setCompressionThreshold(sc.getThreshold());
} else if (packet instanceof ServerLoginSuccess) {
if (VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN &&
!informationForwarded) {
doNotify(ConnectionRequestResults.forDisconnect(
TextComponent.of("Your server did not send a forwarding request to the proxy. Is it set up correctly?")));
connection.disconnect();
return;
}
// The player has been logged on to the backend server.
connection.getMinecraftConnection().setState(StateRegistry.PLAY);
ServerConnection existingConnection = connection.getProxyPlayer().getConnectedServer();
VelocityServerConnection existingConnection = connection.getPlayer().getConnectedServer();
if (existingConnection == null) {
// Strap on the play session handler
connection.getProxyPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(connection.getProxyPlayer()));
connection.getPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(connection.getPlayer()));
} else {
// The previous server connection should become obsolete.
existingConnection.disconnect();
}
// Do we have an outstanding notification? If so, fulfill it.
doNotify(ConnectionRequestResults.SUCCESSFUL);
connection.getMinecraftConnection().setSessionHandler(new BackendPlaySessionHandler(connection));
connection.getProxyPlayer().setConnectedServer(connection);
connection.getPlayer().setConnectedServer(connection);
}
}
@Override
public void deactivated() {
cancelForwardingCheck();
}
@Override
public void exception(Throwable throwable) {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), throwable);
CompletableFuture<ConnectionRequestBuilder.Result> future = connection.getMinecraftConnection().getChannel()
.attr(VelocityServerConnection.CONNECTION_NOTIFIER).getAndSet(null);
if (future != null) {
future.completeExceptionally(throwable);
}
}
private void doNotify(ConnectionRequestBuilder.Result result) {
ChannelPipeline pipeline = connection.getMinecraftConnection().getChannel().pipeline();
ServerConnection.ConnectionNotifier n = pipeline.get(ServerConnection.ConnectionNotifier.class);
if (n != null) {
n.getResult().complete(result);
pipeline.remove(ServerConnection.ConnectionNotifier.class);
CompletableFuture<ConnectionRequestBuilder.Result> future = connection.getMinecraftConnection().getChannel()
.attr(VelocityServerConnection.CONNECTION_NOTIFIER).getAndSet(null);
if (future != null) {
future.complete(result);
}
}
private void cancelForwardingCheck() {
if (forwardingCheckTask != null) {
forwardingCheckTask.cancel(false);
forwardingCheckTask = null;
}
}
private static ByteBuf createForwardingData(String address, GameProfile profile) {
ByteBuf buf = Unpooled.buffer();
ProtocolUtils.writeString(buf, address);
ProtocolUtils.writeUuid(buf, profile.idAsUuid());
ProtocolUtils.writeString(buf, profile.getName());
ProtocolUtils.writeVarInt(buf, profile.getProperties().size());
for (GameProfile.Property property : profile.getProperties()) {
ProtocolUtils.writeString(buf, property.getName());
ProtocolUtils.writeString(buf, property.getValue());
String signature = property.getSignature();
if (signature != null) {
buf.writeBoolean(true);
ProtocolUtils.writeString(buf, signature);
} else {
buf.writeBoolean(false);
static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) {
ByteBuf dataToForward = Unpooled.buffer();
ByteBuf finalData = Unpooled.buffer();
try {
ProtocolUtils.writeString(dataToForward, address);
ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid());
ProtocolUtils.writeString(dataToForward, profile.getName());
ProtocolUtils.writeVarInt(dataToForward, profile.getProperties().size());
for (GameProfile.Property property : profile.getProperties()) {
ProtocolUtils.writeString(dataToForward, property.getName());
ProtocolUtils.writeString(dataToForward, property.getValue());
String signature = property.getSignature();
if (signature != null) {
dataToForward.writeBoolean(true);
ProtocolUtils.writeString(dataToForward, signature);
} else {
dataToForward.writeBoolean(false);
}
}
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
byte[] sig = mac.doFinal();
finalData.writeBytes(sig);
finalData.writeBytes(dataToForward);
return finalData;
} catch (InvalidKeyException e) {
finalData.release();
throw new RuntimeException("Unable to authenticate data", e);
} catch (NoSuchAlgorithmException e) {
// Should never happen
finalData.release();
throw new AssertionError(e);
} finally {
dataToForward.release();
}
return buf;
}
}

Datei anzeigen

@ -1,44 +1,49 @@
package com.velocitypowered.proxy.connection.backend;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.config.IPForwardingMode;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import io.netty.channel.*;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.util.AttributeKey;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.network.Connections.HANDLER;
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.network.Connections.READ_TIMEOUT;
import static com.velocitypowered.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER;
import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.proxy.network.Connections.HANDLER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
import static com.velocitypowered.proxy.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
public class ServerConnection implements MinecraftConnectionAssociation {
static final String CONNECTION_NOTIFIER = "connection-notifier";
public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection {
static final AttributeKey<CompletableFuture<ConnectionRequestBuilder.Result>> CONNECTION_NOTIFIER =
AttributeKey.newInstance("connection-notification-result");
private final ServerInfo serverInfo;
private final ConnectedPlayer proxyPlayer;
private final VelocityServer server;
private MinecraftConnection minecraftConnection;
public ServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
public VelocityServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
this.serverInfo = target;
this.proxyPlayer = proxyPlayer;
this.server = server;
@ -55,12 +60,12 @@ public class ServerConnection implements MinecraftConnectionAssociation {
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND))
.addLast(CONNECTION_NOTIFIER, new ConnectionNotifier(result));
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
ch.attr(CONNECTION_NOTIFIER).set(result);
MinecraftConnection connection = new MinecraftConnection(ch);
connection.setState(StateRegistry.HANDSHAKE);
connection.setAssociation(ServerConnection.this);
connection.setAssociation(VelocityServerConnection.this);
ch.pipeline().addLast(HANDLER, connection);
}
})
@ -72,7 +77,7 @@ public class ServerConnection implements MinecraftConnectionAssociation {
minecraftConnection = future.channel().pipeline().get(MinecraftConnection.class);
// Kick off the connection process
minecraftConnection.setSessionHandler(new LoginSessionHandler(ServerConnection.this));
minecraftConnection.setSessionHandler(new LoginSessionHandler(VelocityServerConnection.this));
startHandshake();
} else {
result.completeExceptionally(future.cause());
@ -93,11 +98,13 @@ public class ServerConnection implements MinecraftConnectionAssociation {
}
private void startHandshake() {
PlayerInfoForwarding forwardingMode = VelocityServer.getServer().getConfiguration().getPlayerInfoForwardingMode();
// Initiate a handshake.
Handshake handshake = new Handshake();
handshake.setNextStatus(StateRegistry.LOGIN_ID);
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.LEGACY) {
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createBungeeForwardingAddress());
} else {
handshake.setServerAddress(serverInfo.getAddress().getHostString());
@ -109,17 +116,9 @@ public class ServerConnection implements MinecraftConnectionAssociation {
minecraftConnection.setProtocolVersion(protocolVersion);
minecraftConnection.setState(StateRegistry.LOGIN);
// Send the server login packet for <=1.12.2 and for 1.13+ servers not using "modern" forwarding.
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ||
VelocityServer.getServer().getConfiguration().getIpForwardingMode() != IPForwardingMode.MODERN) {
ServerLogin login = new ServerLogin();
login.setUsername(proxyPlayer.getUsername());
minecraftConnection.write(login);
}
}
public ConnectedPlayer getProxyPlayer() {
return proxyPlayer;
ServerLogin login = new ServerLogin();
login.setUsername(proxyPlayer.getUsername());
minecraftConnection.write(login);
}
public MinecraftConnection getMinecraftConnection() {
@ -130,6 +129,11 @@ public class ServerConnection implements MinecraftConnectionAssociation {
return serverInfo;
}
@Override
public ConnectedPlayer getPlayer() {
return proxyPlayer;
}
public void disconnect() {
minecraftConnection.close();
minecraftConnection = null;
@ -140,24 +144,13 @@ public class ServerConnection implements MinecraftConnectionAssociation {
return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + serverInfo.getName();
}
static class ConnectionNotifier extends ChannelInboundHandlerAdapter {
private final CompletableFuture<ConnectionRequestBuilder.Result> result;
public ConnectionNotifier(CompletableFuture<ConnectionRequestBuilder.Result> result) {
this.result = result;
}
public CompletableFuture<ConnectionRequestBuilder.Result> getResult() {
return result;
}
public void onComplete() {
result.complete(ConnectionRequestResults.SUCCESSFUL);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
result.completeExceptionally(cause);
}
@Override
public void sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
Preconditions.checkNotNull(identifier, "identifier");
Preconditions.checkNotNull(data, "data");
PluginMessage message = new PluginMessage();
message.setChannel(identifier.getId());
message.setData(data);
minecraftConnection.write(message);
}
}

Datei anzeigen

@ -1,47 +1,41 @@
package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.proxy.messages.ChannelSide;
import com.velocitypowered.api.proxy.messages.MessageHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.data.scoreboard.Objective;
import com.velocitypowered.proxy.data.scoreboard.Score;
import com.velocitypowered.proxy.data.scoreboard.Scoreboard;
import com.velocitypowered.proxy.data.scoreboard.Team;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.remap.EntityIdRemapper;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import com.velocitypowered.proxy.util.ThrowableUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.EventLoop;
import io.netty.util.ReferenceCountUtil;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* Handles communication with the connected Minecraft client. This is effectively the primary nerve center that
* joins backend servers with players.
*/
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
private static final int MAX_PLUGIN_CHANNELS = 128;
private final ConnectedPlayer player;
private ScheduledFuture<?> pingTask;
private long lastPing = -1;
private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>();
private final Set<String> clientPluginMsgChannels = new HashSet<>();
private int currentDimension;
private Scoreboard serverScoreboard = new Scoreboard();
private EntityIdRemapper idRemapper;
public ClientPlaySessionHandler(ConnectedPlayer player) {
@ -50,16 +44,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void activated() {
EventLoop loop = player.getConnection().getChannel().eventLoop();
pingTask = loop.scheduleAtFixedRate(this::ping, 5, 15, TimeUnit.SECONDS);
}
private void ping() {
long randomId = ThreadLocalRandom.current().nextInt();
lastPing = randomId;
KeepAlive keepAlive = new KeepAlive();
keepAlive.setRandomId(randomId);
player.getConnection().write(keepAlive);
PluginMessage message;
if (player.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
message = PluginMessageUtil.constructChannelsPacket("minecraft:register", VelocityServer.getServer().getChannelRegistrar().getModernChannelIds());
} else {
message = PluginMessageUtil.constructChannelsPacket("REGISTER", VelocityServer.getServer().getChannelRegistrar().getLegacyChannelIds());
}
player.getConnection().write(message);
}
@Override
@ -67,11 +58,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (packet instanceof KeepAlive) {
KeepAlive keepAlive = (KeepAlive) packet;
if (keepAlive.getRandomId() != lastPing) {
throw new IllegalStateException("Client sent invalid keepAlive; expected " + lastPing + ", got " + keepAlive.getRandomId());
// The last keep alive we got was probably from a different server. Let's ignore it, and hope the next
// ping is alright.
return;
}
// Do not forward the packet to the player's server, because we handle pings for all servers already.
return;
}
if (packet instanceof ClientSettings) {
@ -80,10 +70,49 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
if (packet instanceof Chat) {
// Try to handle any commands on the proxy. If that fails, send it onto the client.
Chat chat = (Chat) packet;
if (chat.getMessage().equals("/connect")) {
ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25566));
player.createConnectionRequest(info).fireAndForget();
String msg = ((Chat) packet).getMessage();
if (msg.startsWith("/")) {
try {
if (!VelocityServer.getServer().getCommandManager().execute(player, msg.substring(1))) {
player.getConnectedServer().getMinecraftConnection().write(chat);
}
} catch (Exception e) {
logger.info("Exception occurred while running command for {}", player.getProfile().getName(), e);
player.sendMessage(TextComponent.of("An error occurred while running this command.", TextColor.RED));
return;
}
} else {
player.getConnectedServer().getMinecraftConnection().write(chat);
}
return;
}
if (packet instanceof TabCompleteRequest) {
TabCompleteRequest req = (TabCompleteRequest) packet;
int lastSpace = req.getCommand().indexOf(' ');
if (!req.isAssumeCommand() && lastSpace != -1) {
String command = req.getCommand().substring(1);
try {
Optional<List<String>> offers = VelocityServer.getServer().getCommandManager().offerSuggestions(player, command);
if (offers.isPresent()) {
TabCompleteResponse response = new TabCompleteResponse();
response.setTransactionId(req.getTransactionId());
response.setStart(lastSpace);
response.setLength(req.getCommand().length() - lastSpace);
for (String s : offers.get()) {
response.getOffers().add(new TabCompleteResponse.Offer(s, null));
}
player.getConnection().write(response);
} else {
player.getConnectedServer().getMinecraftConnection().write(packet);
}
} catch (Exception e) {
logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", e);
}
return;
}
}
@ -106,11 +135,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void disconnected() {
player.teardown();
if (pingTask != null && !pingTask.isCancelled()) {
pingTask.cancel(false);
pingTask = null;
}
VelocityServer.getServer().getEventManager().fireAndForget(new DisconnectEvent(player));
}
@Override
@ -123,21 +148,22 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
public void handleBackendJoinGame(JoinGame joinGame) {
lastPing = Long.MIN_VALUE; // reset last ping
if (!spawned) {
// nothing special to do here
spawned = true;
currentDimension = joinGame.getDimension();
player.getConnection().delayedWrite(joinGame);
idRemapper = EntityIdRemapper.getMapper(joinGame.getEntityId(), player.getConnection().getProtocolVersion());
} else {
// In order to handle switching to another server we will need send three packets:
// Ah, this is the meat and potatoes of the whole venture!
//
// In order to handle switching to another server, you will need to send three packets:
//
// - The join game packet from the backend server
// - A respawn packet with a different dimension
// - Another respawn with the correct dimension
//
// We can't simply ignore the packet with the different dimension. If you try to be smart about it it doesn't
// work.
// The two respawns with different dimensions are required, otherwise the client gets confused.
//
// Most notably, by having the client accept the join game packet, we can work around the need to perform
// entity ID rewrites, eliminating potential issues from rewriting packets and improving compatibility with
@ -147,13 +173,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
player.getConnection().delayedWrite(new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType()));
player.getConnection().delayedWrite(new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType()));
currentDimension = joinGame.getDimension();
}
// Resend client settings packet to remote server if we have it, this preserves client settings across
// transitions.
if (player.getClientSettings() != null) {
player.getConnectedServer().getMinecraftConnection().delayedWrite(player.getClientSettings());
}
// Remove old boss bars.
@ -165,15 +184,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
serverBossBars.clear();
// Remove scoreboard junk.
clearServerScoreboard();
// Tell the server about this client's plugin messages. Velocity will forward them on to the client.
if (!clientPluginMsgChannels.isEmpty()) {
Collection<String> toRegister = new HashSet<>(clientPluginMsgChannels);
if (player.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
toRegister.addAll(VelocityServer.getServer().getChannelRegistrar().getModernChannelIds());
} else {
toRegister.addAll(VelocityServer.getServer().getChannelRegistrar().getLegacyChannelIds());
}
if (!toRegister.isEmpty()) {
String channel = player.getConnection().getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13 ?
"minecraft:register" : "REGISTER";
player.getConnectedServer().getMinecraftConnection().delayedWrite(
PluginMessageUtil.constructChannelsPacket(channel, clientPluginMsgChannels));
player.getConnectedServer().getMinecraftConnection().delayedWrite(PluginMessageUtil.constructChannelsPacket(
channel, toRegister));
}
// Flush everything
@ -181,10 +203,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
player.getConnectedServer().getMinecraftConnection().flush();
}
public void setCurrentDimension(int currentDimension) {
this.currentDimension = currentDimension;
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
@ -204,7 +222,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
if (actuallyRegistered.size() > 0) {
logger.info("Rewritten register packet: {}", actuallyRegistered);
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered);
player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket);
}
@ -222,88 +239,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return;
}
// We're going to forward on the original packet.
player.getConnectedServer().getMinecraftConnection().write(packet);
}
public void handleServerScoreboardPacket(MinecraftPacket packet) {
if (packet instanceof ScoreboardDisplay) {
ScoreboardDisplay sd = (ScoreboardDisplay) packet;
serverScoreboard.setPosition(sd.getPosition());
serverScoreboard.setDisplayName(sd.getDisplayName());
MessageHandler.ForwardStatus status = VelocityServer.getServer().getChannelRegistrar().handlePluginMessage(
player, ChannelSide.FROM_CLIENT, packet);
if (status == MessageHandler.ForwardStatus.FORWARD) {
// We're going to forward on the original packet.
player.getConnectedServer().getMinecraftConnection().write(packet);
}
if (packet instanceof ScoreboardObjective) {
ScoreboardObjective so = (ScoreboardObjective) packet;
switch (so.getMode()) {
case ScoreboardObjective.ADD:
Objective o = new Objective(so.getId());
o.setDisplayName(so.getDisplayName());
o.setType(so.getType());
serverScoreboard.getObjectives().put(so.getId(), o);
break;
case ScoreboardObjective.REMOVE:
serverScoreboard.getObjectives().remove(so.getId());
break;
}
}
if (packet instanceof ScoreboardSetScore) {
ScoreboardSetScore sss = (ScoreboardSetScore) packet;
Objective objective = serverScoreboard.getObjectives().get(sss.getObjective());
if (objective == null) {
return;
}
switch (sss.getAction()) {
case ScoreboardSetScore.CHANGE:
Score score = new Score(sss.getEntity(), sss.getValue());
objective.getScores().put(sss.getEntity(), score);
break;
case ScoreboardSetScore.REMOVE:
objective.getScores().remove(sss.getEntity());
break;
}
}
if (packet instanceof ScoreboardTeam) {
ScoreboardTeam st = (ScoreboardTeam) packet;
switch (st.getMode()) {
case ScoreboardTeam.ADD:
// TODO: Preserve other team information? We might not need to...
Team team = new Team(st.getId());
serverScoreboard.getTeams().put(st.getId(), team);
break;
case ScoreboardTeam.REMOVE:
serverScoreboard.getTeams().remove(st.getId());
break;
}
}
}
private void clearServerScoreboard() {
for (Objective objective : serverScoreboard.getObjectives().values()) {
for (Score score : objective.getScores().values()) {
ScoreboardSetScore sss = new ScoreboardSetScore();
sss.setObjective(objective.getId());
sss.setAction(ScoreboardSetScore.REMOVE);
sss.setEntity(score.getTarget());
player.getConnection().delayedWrite(sss);
}
ScoreboardObjective so = new ScoreboardObjective();
so.setId(objective.getId());
so.setMode(ScoreboardObjective.REMOVE);
player.getConnection().delayedWrite(so);
}
for (Team team : serverScoreboard.getTeams().values()) {
ScoreboardTeam st = new ScoreboardTeam();
st.setId(team.getId());
st.setMode(ScoreboardTeam.REMOVE);
player.getConnection().delayedWrite(st);
}
serverScoreboard = new Scoreboard();
}
public Set<String> getClientPluginMsgChannels() {
@ -313,4 +254,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
public EntityIdRemapper getIdRemapper() {
return idRemapper;
}
public void setLastPing(long lastPing) {
this.lastPing = lastPing;
}
}

Datei anzeigen

@ -2,21 +2,29 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.gson.JsonObject;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.permission.PermissionFunction;
import com.velocitypowered.api.permission.PermissionProvider;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.util.MessagePosition;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.packet.Chat;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.util.ThrowableUtils;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
@ -25,8 +33,8 @@ import net.kyori.text.serializer.ComponentSerializers;
import net.kyori.text.serializer.PlainComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import javax.annotation.Nonnull;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Optional;
@ -35,19 +43,23 @@ import java.util.concurrent.CompletableFuture;
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private static final PlainComponentSerializer PASS_THRU_TRANSLATE = new PlainComponentSerializer((c) -> "", TranslatableComponent::key);
public static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class);
private final GameProfile profile;
private final MinecraftConnection connection;
private final InetSocketAddress virtualHost;
private final GameProfile profile;
private PermissionFunction permissionFunction = null;
private int tryIndex = 0;
private ServerConnection connectedServer;
private VelocityServerConnection connectedServer;
private ClientSettings clientSettings;
private ServerConnection connectionInFlight;
private VelocityServerConnection connectionInFlight;
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection) {
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection, InetSocketAddress virtualHost) {
this.profile = profile;
this.connection = connection;
this.virtualHost = virtualHost;
}
@Override
@ -61,8 +73,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
@Override
public Optional<ServerInfo> getCurrentServer() {
return connectedServer != null ? Optional.of(connectedServer.getServerInfo()) : Optional.empty();
public Optional<ServerConnection> getCurrentServer() {
return Optional.ofNullable(connectedServer);
}
public GameProfile getProfile() {
@ -78,13 +90,27 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.ofNullable(virtualHost);
}
public void setPermissionFunction(PermissionFunction permissionFunction) {
this.permissionFunction = permissionFunction;
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
}
@Override
public void sendMessage(@Nonnull Component component, @Nonnull MessagePosition position) {
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
@Override
public void sendMessage(@NonNull Component component, @NonNull MessagePosition position) {
Preconditions.checkNotNull(component, "component");
Preconditions.checkNotNull(position, "position");
@ -107,11 +133,28 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
@Override
public ConnectionRequestBuilder createConnectionRequest(@Nonnull ServerInfo info) {
public ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info) {
return new ConnectionRequestBuilderImpl(info);
}
public ServerConnection getConnectedServer() {
@Override
public void setHeaderAndFooter(@NonNull Component header, @NonNull Component footer) {
Preconditions.checkNotNull(header, "header");
Preconditions.checkNotNull(footer, "footer");
connection.write(HeaderAndFooter.create(header, footer));
}
@Override
public void clearHeaderAndFooter() {
connection.write(HeaderAndFooter.reset());
}
@Override
public void disconnect(Component reason) {
connection.closeWith(Disconnect.create(reason));
}
public VelocityServerConnection getConnectedServer() {
return connectedServer;
}
@ -127,7 +170,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
String error = ThrowableUtils.briefDescription(throwable);
String userMessage;
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
logger.error("{}: exception occurred in connection to {}", this, info.getName(), throwable);
userMessage = "Exception in server " + info.getName();
} else {
logger.error("{}: unable to connect to server {}", this, info.getName(), throwable);
@ -153,9 +195,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
public void handleConnectionException(ServerInfo info, Component disconnectReason) {
connectionInFlight = null;
if (connectedServer == null || connectedServer.getServerInfo().equals(info)) {
// The player isn't yet connected to a server or they are already connected to the server
// they're disconnected from.
if (connectedServer == null) {
// The player isn't yet connected to a server.
Optional<ServerInfo> nextServer = getNextServerToTry();
if (nextServer.isPresent()) {
createConnectionRequest(nextServer.get()).fireAndForget();
} else {
connection.closeWith(Disconnect.create(disconnectReason));
}
} else if (connectedServer.getServerInfo().equals(info)) {
// Already connected to the server being disconnected from.
// TODO: ServerKickEvent
connection.closeWith(Disconnect.create(disconnectReason));
} else {
connection.write(Chat.create(disconnectReason));
@ -187,11 +237,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
}
// Otherwise, initiate the connection.
ServerConnection connection = new ServerConnection(request.getServer(), this, VelocityServer.getServer());
return connection.connect();
ServerPreConnectEvent event = new ServerPreConnectEvent(this, ServerPreConnectEvent.ServerResult.allowed(request.getServer()));
return VelocityServer.getServer().getEventManager().fire(event)
.thenCompose((newEvent) -> {
if (!newEvent.getResult().isAllowed()) {
return CompletableFuture.completedFuture(
ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED)
);
}
return new VelocityServerConnection(newEvent.getResult().getInfo().get(), this, VelocityServer.getServer()).connect();
});
}
public void setConnectedServer(ServerConnection serverConnection) {
public void setConnectedServer(VelocityServerConnection serverConnection) {
if (this.connectedServer != null && !serverConnection.getServerInfo().equals(connectedServer.getServerInfo())) {
this.tryIndex = 0;
}
@ -209,6 +268,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
if (connectedServer != null) {
connectedServer.disconnect();
}
VelocityServer.getServer().unregisterConnection(this);
}
@Override
@ -216,10 +276,25 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return "[connected player] " + getProfile().getName() + " (" + getRemoteAddress() + ")";
}
@Override
public boolean hasPermission(String permission) {
return permissionFunction.getPermissionSetting(permission).asBoolean();
}
@Override
public void sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
Preconditions.checkNotNull(identifier, "identifier");
Preconditions.checkNotNull(data, "data");
PluginMessage message = new PluginMessage();
message.setChannel(identifier.getId());
message.setData(data);
connection.write(message);
}
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final ServerInfo info;
public ConnectionRequestBuilderImpl(ServerInfo info) {
ConnectionRequestBuilderImpl(ServerInfo info) {
this.info = Preconditions.checkNotNull(info, "info");
}
@ -236,7 +311,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
@Override
public void fireAndForget() {
connect()
.whenComplete((status, throwable) -> {
.whenCompleteAsync((status, throwable) -> {
if (throwable != null) {
handleConnectionException(info, throwable);
return;
@ -256,7 +331,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
handleConnectionException(info, Disconnect.create(status.getReason().orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR)));
break;
}
});
}, connection.getChannel().eventLoop());
}
}
}

Datei anzeigen

@ -1,14 +1,28 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.event.connection.ConnectionHandshakeEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import com.velocitypowered.proxy.protocol.packet.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
import net.kyori.text.format.TextColor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Optional;
public class HandshakeSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection;
@ -19,16 +33,24 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
@Override
public void handle(MinecraftPacket packet) {
if (packet instanceof LegacyPing || packet instanceof LegacyHandshake) {
connection.setProtocolVersion(ProtocolConstants.LEGACY);
handleLegacy(packet);
return;
}
if (!(packet instanceof Handshake)) {
throw new IllegalArgumentException("Did not expect packet " + packet.getClass().getName());
}
InitialInboundConnection ic = new InitialInboundConnection(connection, (Handshake) packet);
Handshake handshake = (Handshake) packet;
switch (handshake.getNextStatus()) {
case StateRegistry.STATUS_ID:
connection.setState(StateRegistry.STATUS);
connection.setProtocolVersion(handshake.getProtocolVersion());
connection.setSessionHandler(new StatusSessionHandler(connection));
connection.setSessionHandler(new StatusSessionHandler(connection, ic));
break;
case StateRegistry.LOGIN_ID:
connection.setState(StateRegistry.LOGIN);
@ -37,12 +59,70 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
return;
} else {
connection.setSessionHandler(new LoginSessionHandler(connection));
InetAddress address = ((InetSocketAddress) connection.getChannel().remoteAddress()).getAddress();
if (!VelocityServer.getServer().getIpAttemptLimiter().attempt(address)) {
connection.closeWith(Disconnect.create(TextComponent.of("You are logging in too fast, try again later.")));
return;
}
VelocityServer.getServer().getEventManager().fireAndForget(new ConnectionHandshakeEvent(ic));
connection.setSessionHandler(new LoginSessionHandler(connection, ic));
}
break;
default:
throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus());
}
}
@Override
public void handleUnknown(ByteBuf buf) {
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
}
private void handleLegacy(MinecraftPacket packet) {
if (packet instanceof LegacyPing) {
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
ServerPing ping = new ServerPing(
new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
null
);
ProxyPingEvent event = new ProxyPingEvent(new LegacyInboundConnection(connection), ping);
VelocityServer.getServer().getEventManager().fire(event)
.thenRunAsync(() -> {
// The disconnect packet is the same as the server response one.
connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(event.getPing())));
}, connection.getChannel().eventLoop());
} else if (packet instanceof LegacyHandshake) {
connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED)));
}
}
private static class LegacyInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private LegacyInboundConnection(MinecraftConnection connection) {
this.connection = connection;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.empty();
}
@Override
public boolean isActive() {
return !connection.isClosed();
}
@Override
public int getProtocolVersion() {
return 0;
}
}
}

Datei anzeigen

@ -0,0 +1,38 @@
package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.packet.Handshake;
import java.net.InetSocketAddress;
import java.util.Optional;
class InitialInboundConnection implements InboundConnection {
private final MinecraftConnection connection;
private final Handshake handshake;
InitialInboundConnection(MinecraftConnection connection, Handshake handshake) {
this.connection = connection;
this.handshake = handshake;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
@Override
public Optional<InetSocketAddress> getVirtualHost() {
return Optional.of(InetSocketAddress.createUnresolved(handshake.getServerAddress(), handshake.getPort()));
}
@Override
public boolean isActive() {
return connection.getChannel().isActive();
}
@Override
public int getProtocolVersion() {
return connection.getProtocolVersion();
}
}

Datei anzeigen

@ -1,8 +1,15 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
import com.velocitypowered.api.event.permission.PermissionsSetupEvent;
import com.velocitypowered.api.event.player.GameProfileRequestEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
@ -10,8 +17,9 @@ import com.velocitypowered.proxy.protocol.packet.*;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.api.server.ServerInfo;
import com.velocitypowered.proxy.util.EncryptionUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
@ -19,6 +27,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
@ -30,82 +39,115 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class);
private static final String MOJANG_SERVER_AUTH_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
private final MinecraftConnection inbound;
private final InboundConnection apiInbound;
private ServerLogin login;
private byte[] verify;
private int playerInfoId;
public LoginSessionHandler(MinecraftConnection inbound) {
public LoginSessionHandler(MinecraftConnection inbound, InboundConnection apiInbound) {
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
this.apiInbound = Preconditions.checkNotNull(apiInbound, "apiInbound");
}
@Override
public void activated() {
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
LoginPluginMessage message = new LoginPluginMessage();
playerInfoId = ThreadLocalRandom.current().nextInt();
message.setId(playerInfoId);
message.setChannel(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL);
message.setData(Unpooled.EMPTY_BUFFER);
inbound.write(message);
}
}
@Override
public void handle(MinecraftPacket packet) throws Exception {
public void handle(MinecraftPacket packet) {
if (packet instanceof LoginPluginResponse) {
LoginPluginResponse lpr = (LoginPluginResponse) packet;
if (lpr.getId() == playerInfoId && lpr.isSuccess()) {
// Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening.
inbound.closeWith(Disconnect.create(
TextComponent.of("Running Velocity behind Velocity isn't supported.", TextColor.RED)
));
if (lpr.getId() == playerInfoId) {
if (lpr.isSuccess()) {
// Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening.
inbound.closeWith(Disconnect.create(
TextComponent.of("Running Velocity behind Velocity isn't supported.", TextColor.RED)
));
} else {
// Proceed with the regular login process.
beginPreLogin();
}
}
} else if (packet instanceof ServerLogin) {
this.login = (ServerLogin) packet;
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
LoginPluginMessage message = new LoginPluginMessage();
playerInfoId = ThreadLocalRandom.current().nextInt();
message.setId(playerInfoId);
message.setChannel(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL);
message.setData(Unpooled.EMPTY_BUFFER);
inbound.write(message);
} else {
// Offline-mode, don't try to request encryption.
handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername()));
beginPreLogin();
}
} else if (packet instanceof EncryptionResponse) {
KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair();
EncryptionResponse response = (EncryptionResponse) packet;
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken());
if (!Arrays.equals(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
try {
KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair();
EncryptionResponse response = (EncryptionResponse) packet;
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken());
if (!Arrays.equals(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
}
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret());
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString();
VelocityServer.getServer().getHttpClient()
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
.thenAcceptAsync(profileResponse -> {
if (inbound.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
initializePlayer(VelocityServer.GSON.fromJson(profileResponse, GameProfile.class), true);
}, inbound.getChannel().eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
inbound.close();
return null;
});
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e);
inbound.close();
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret());
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString();
VelocityServer.getServer().getHttpClient()
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
.thenAcceptAsync(profileResponse -> {
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class);
handleSuccessfulLogin(profile);
}, inbound.getChannel().eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
inbound.close();
return null;
});
}
}
private void beginPreLogin() {
PreLoginEvent event = new PreLoginEvent(apiInbound, login.getUsername());
VelocityServer.getServer().getEventManager().fire(event)
.thenRunAsync(() -> {
if (inbound.isClosed()) {
// The player was disconnected
return;
}
PreLoginComponentResult result = event.getResult();
if (!result.isAllowed()) {
// The component is guaranteed to be provided if the connection was denied.
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
return;
}
if (VelocityServer.getServer().getConfiguration().isOnlineMode() || result.isOnlineModeAllowed()) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
} else {
initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()), false);
}
}, inbound.getChannel().eventLoop());
}
private EncryptionRequest generateRequest() {
byte[] verify = new byte[4];
ThreadLocalRandom.current().nextBytes(verify);
@ -116,9 +158,40 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
return request;
}
private void handleSuccessfulLogin(GameProfile profile) {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(profile, inbound);
private void initializePlayer(GameProfile profile, boolean onlineMode) {
GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, onlineMode);
VelocityServer.getServer().getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(profileEvent.getGameProfile(), inbound,
apiInbound.getVirtualHost().orElse(null));
return VelocityServer.getServer().getEventManager().fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS))
.thenCompose(event -> {
// wait for permissions to load, then set the players permission function
player.setPermissionFunction(event.createFunction(player));
// then call & wait for the login event
return VelocityServer.getServer().getEventManager().fire(new LoginEvent(player));
})
// then complete the connection
.thenAcceptAsync(event -> {
if (inbound.isClosed()) {
// The player was disconnected
return;
}
if (!event.getResult().isAllowed()) {
// The component is guaranteed to be provided if the connection was denied.
inbound.closeWith(Disconnect.create(event.getResult().getReason().get()));
return;
}
handleProxyLogin(player);
}, inbound.getChannel().eventLoop());
});
}
private void handleProxyLogin(ConnectedPlayer player) {
Optional<ServerInfo> toTry = player.getNextServerToTry();
if (!toTry.isPresent()) {
player.close(TextComponent.of("No available servers", TextColor.RED));
@ -132,14 +205,24 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
}
ServerLoginSuccess success = new ServerLoginSuccess();
success.setUsername(profile.getName());
success.setUuid(profile.idAsUuid());
success.setUsername(player.getUsername());
success.setUuid(player.getUniqueId());
inbound.write(success);
logger.info("{} has connected", player);
inbound.setAssociation(player);
inbound.setState(StateRegistry.PLAY);
if (!VelocityServer.getServer().registerConnection(player)) {
inbound.closeWith(Disconnect.create(TextComponent.of("You are already on this proxy!", TextColor.RED)));
}
logger.info("{} has connected", player);
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
player.createConnectionRequest(toTry.get()).fireAndForget();
}
@Override
public void handleUnknown(ByteBuf buf) {
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
}
}

Datei anzeigen

@ -1,28 +1,34 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packet.StatusPing;
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.data.ServerPing;
import com.velocitypowered.api.proxy.server.ServerPing;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
public class StatusSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection;
private final InboundConnection inboundWrapper;
public StatusSessionHandler(MinecraftConnection connection) {
public StatusSessionHandler(MinecraftConnection connection, InboundConnection inboundWrapper) {
this.connection = connection;
this.inboundWrapper = inboundWrapper;
}
@Override
public void handle(MinecraftPacket packet) {
Preconditions.checkArgument(packet instanceof StatusPing|| packet instanceof StatusRequest,
Preconditions.checkArgument(packet instanceof StatusPing || packet instanceof StatusRequest,
"Unrecognized packet type " + packet.getClass().getName());
if (packet instanceof StatusPing) {
@ -34,15 +40,22 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
// Status request
ServerPing ping = new ServerPing(
new ServerPing.Version(connection.getProtocolVersion(), "Velocity 1.9-1.13"),
new ServerPing.Players(0, configuration.getShowMaxPlayers()),
int shownVersion = ProtocolConstants.isSupported(connection.getProtocolVersion()) ? connection.getProtocolVersion() :
ProtocolConstants.MAXIMUM_GENERIC_VERSION;
ServerPing initialPing = new ServerPing(
new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING),
new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()),
configuration.getMotdComponent(),
null
configuration.getFavicon()
);
StatusResponse response = new StatusResponse();
response.setStatus(VelocityServer.GSON.toJson(ping));
connection.write(response);
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
VelocityServer.getServer().getEventManager().fire(event)
.thenRunAsync(() -> {
StatusResponse response = new StatusResponse();
response.setStatus(VelocityServer.GSON.toJson(event.getPing()));
connection.write(response);
}, connection.getChannel().eventLoop());
}
@Override

Datei anzeigen

@ -3,6 +3,7 @@ package com.velocitypowered.proxy.connection.util;
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
import com.velocitypowered.proxy.protocol.packet.Disconnect;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.serializer.ComponentSerializers;
import java.util.Optional;
@ -42,4 +43,18 @@ public class ConnectionRequestResults {
}
};
}
public static ConnectionRequestBuilder.Result forDisconnect(TextComponent component) {
return new ConnectionRequestBuilder.Result() {
@Override
public ConnectionRequestBuilder.Status getStatus() {
return ConnectionRequestBuilder.Status.SERVER_DISCONNECTED;
}
@Override
public Optional<Component> getReason() {
return Optional.of(component);
}
};
}
}

Datei anzeigen

@ -0,0 +1,52 @@
package com.velocitypowered.proxy.console;
import com.velocitypowered.proxy.VelocityServer;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import net.minecrell.terminalconsole.SimpleTerminalConsole;
import org.jline.reader.*;
import java.util.List;
import java.util.Optional;
public final class VelocityConsole extends SimpleTerminalConsole {
private final VelocityServer server;
public VelocityConsole(VelocityServer server) {
this.server = server;
}
@Override
protected LineReader buildReader(LineReaderBuilder builder) {
return super.buildReader(builder
.appName("Velocity")
.completer((reader, parsedLine, list) -> {
Optional<List<String>> o = server.getCommandManager().offerSuggestions(server.getConsoleCommandSource(), parsedLine.line());
o.ifPresent(offers -> {
for (String offer : offers) {
list.add(new Candidate(offer));
}
});
})
);
}
@Override
protected boolean isRunning() {
return !this.server.isShutdown();
}
@Override
protected void runCommand(String command) {
if (!this.server.getCommandManager().execute(this.server.getConsoleCommandSource(), command)) {
server.getConsoleCommandSource().sendMessage(TextComponent.of("Command not found.", TextColor.RED));
}
}
@Override
protected void shutdown() {
this.server.shutdown();
}
}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden Mehr anzeigen