feat: 完成网易认证功能支持
Some checks failed
Release Project / build (push) Failing after 5m51s

This commit is contained in:
2025-12-24 22:42:59 +08:00
commit 9b21e1a9b9
18 changed files with 913 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
name: Release Project
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: 21
distribution: temurin
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5.0.0
with:
gradle-version: wrapper
cache-read-only: false
cache-write-only: false
- name: Make gradlew executable
run: chmod +x gradlew
- name: Build & Publish
env:
ORG_GRADLE_PROJECT_MAVEN_AIRGAME_USERNAME: ${{ secrets.MAVEN_AIRGAME_USERNAME }}
ORG_GRADLE_PROJECT_MAVEN_AIRGAME_PASSWORD: ${{ secrets.MAVEN_AIRGAME_PASSWORD }}
run: ./gradlew build --info --console plain --no-daemon
- name: Generate Changelog
uses: orhun/git-cliff-action@v4
id: git-cliff
with:
args: --latest --strip header
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_GITHUB_TOKEN }}
- name: Publish to Release
uses: softprops/action-gh-release@v2
with:
files: build/*.jar
name: ${{ github.ref_name }}
body: ${{ steps.git-cliff.outputs.content }}

46
.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
.gradle
.idea
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Kotlin ###
.kotlin
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

22
README.md Normal file
View File

@@ -0,0 +1,22 @@
# VelocityNeteaseAuth
为 Velocity 代理端添加网易验证使用mixin注入代码无需修改代理端核心。
# 使用方式
1. 下载并安装 [ignite](https://github.com/vectrix-space/ignite)
2. 将本插件放到 mods 文件夹
3. 启动服务端,服务端根目录生成 `netease.yml` 文件
4. 编辑 `netease.yml` 以符合服务器预期
5. 重新启动服务端
# 如何为 Velocity 使用 ignite
1. 关闭 Velocity 服务端
2. 下载 [ignite](https://github.com/vectrix-space/ignite) 文件,将其命名为 ignite.jar并放到服务端根目录
3. 将 Velocity 核心文件重命名为 `velocity.jar`
4. 修改启动脚本,将 `java -jar velocity.jar` 改为 `java -Dignite.locator=velocity -jar ignite.jar`
- 主要目的是不再直接启动 velocity而是让 ignite 去启动 velocity
- 这样 ignite 比 velocity 先启动,就有能力在运行时动态修改或注入代码到 velocity 中
- 启动参数中的 `-Dignite.locator=velocity` 主要是告诉 ignite要启动的服务端是 `Velocity`(因为 ignite 同时也支持 Spigot/Paper
5. 启动服务端

36
build.gradle.kts Normal file
View File

@@ -0,0 +1,36 @@
plugins {
id("java")
id("me.glicz.access-widen") version "3.0.0"
}
group = "cn.hamster3.server.mixin"
version = "1.0.0"
description = "为 Velocity 添加网易认证注入"
base {
archivesName = "VelocityNeteaseAuth"
}
repositories {
maven("https://maven.airgame.net/maven-public")
}
dependencies {
compileOnly("space.vectrix.ignite:ignite-api:1.1.0")
compileOnly("org.spongepowered:mixin:0.8.7")
compileOnly("io.github.llamalad7:mixinextras-common:0.5.0")
annotationProcessor("io.github.llamalad7:mixinextras-common:0.5.0")
compileOnly(accessWiden(rootProject.fileTree("./libs")))
}
accessWiden {
accessWideners.from(
fileTree(
mapOf(
"dir" to "src/main/resources",
"include" to "*.accesswidener"
)
)
)
}

1
gradle.properties Normal file
View File

@@ -0,0 +1 @@
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
#Wed Dec 24 19:29:45 CST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
# distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Normal file
View File

@@ -0,0 +1,234 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

BIN
libs/velocity.jar Normal file

Binary file not shown.

9
settings.gradle.kts Normal file
View File

@@ -0,0 +1,9 @@
pluginManagement {
repositories {
maven("https://maven.airgame.net/maven-public")
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "velocity-netease-auth"

View File

@@ -0,0 +1,53 @@
package cn.hamster3.mc.auth.config;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Map;
public class NeteaseServerConfig {
private static boolean loaded = false;
private static String authUrl = "http://192.168.0.100:9999/check";
private static String gameId = "12345678901234567";
static {
init();
}
@SuppressWarnings("CallToPrintStackTrace")
public static void init() {
if (loaded) {
return;
}
loaded = true;
File file = new File("netease.yml");
if (!file.exists()) {
try (InputStream stream = NeteaseServerConfig.class.getResourceAsStream("/netease.yml")) {
if (stream != null) {
Files.copy(stream, file.toPath());
}
} catch (Exception e) {
e.printStackTrace();
}
}
try {
String readString = Files.readString(file.toPath());
Map<String, String> map = new Yaml().load(readString);
authUrl = map.getOrDefault("auth-url", "http://192.168.0.100:9999/check");
gameId = map.getOrDefault("game-id", "12345678901234567");
} catch (IOException e) {
e.printStackTrace();
}
}
public static String getAuthUrl() {
return authUrl;
}
public static String getGameId() {
return gameId;
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package cn.hamster3.mc.auth.data;
import com.velocitypowered.api.util.GameProfile;
import java.util.List;
@SuppressWarnings("unused")
public class NeteaseAuthResponse {
private int code;
private String message;
private String details;
private ResponseEntity entity;
public NeteaseAuthResponse(int code, String message, String details, ResponseEntity entity) {
this.code = code;
this.message = message;
this.details = details;
this.entity = entity;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public ResponseEntity getEntity() {
return entity;
}
public void setEntity(ResponseEntity entity) {
this.entity = entity;
}
public static class ResponseEntity {
private String id;
private String name;
private List<GameProfile.Property> properties;
public ResponseEntity(String id, String name, List<GameProfile.Property> properties) {
this.id = id;
this.name = name;
this.properties = properties;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<GameProfile.Property> getProperties() {
return properties;
}
public void setProperties(List<GameProfile.Property> properties) {
this.properties = properties;
}
@Override
public String toString() {
return "ResponseEntity{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", properties=" + properties +
'}';
}
}
@Override
public String toString() {
return "NeteaseAuthResponse{" +
"code=" + code +
", message='" + message + '\'' +
", details='" + details + '\'' +
", entity=" + entity +
'}';
}
}

View File

@@ -0,0 +1,207 @@
package cn.hamster3.mc.auth.mixin;
import cn.hamster3.mc.auth.config.NeteaseServerConfig;
import cn.hamster3.mc.auth.data.NeteaseAuthResponse;
import com.google.common.primitives.Longs;
import com.google.gson.JsonObject;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.AuthSessionHandler;
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
import com.velocitypowered.proxy.connection.client.LoginInboundConnection;
import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.Logger;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.util.ArrayList;
import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON;
import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa;
import static com.velocitypowered.proxy.crypto.EncryptionUtils.generateServerId;
@Mixin(InitialLoginSessionHandler.class)
public abstract class InitialLoginSessionHandlerMixin {
@Shadow
protected abstract void assertState(InitialLoginSessionHandler.LoginState expectedState);
@Shadow
private InitialLoginSessionHandler.LoginState currentState;
@Shadow
private ServerLoginPacket login;
@Shadow
private byte[] verify;
@Shadow
@Final
private VelocityServer server;
@Shadow
@Final
private LoginInboundConnection inbound;
@Shadow
@Final
private MinecraftConnection mcConnection;
@Shadow
@Final
private static Logger logger;
@Inject(at = @At(value = "HEAD"), method = "handle(Lcom/velocitypowered/proxy/protocol/packet/EncryptionResponsePacket;)Z", cancellable = true)
public void injectHandle(EncryptionResponsePacket packet, CallbackInfoReturnable<Boolean> cir) {
assertState(InitialLoginSessionHandler.LoginState.ENCRYPTION_REQUEST_SENT);
currentState = InitialLoginSessionHandler.LoginState.ENCRYPTION_RESPONSE_RECEIVED;
ServerLoginPacket login = this.login;
if (login == null) {
throw new IllegalStateException("No ServerLogin packet received yet.");
}
if (verify.length == 0) {
throw new IllegalStateException("No EncryptionRequest packet sent yet.");
}
try {
KeyPair serverKeyPair = server.getServerKeyPair();
if (inbound.getIdentifiedKey() != null) {
IdentifiedKey playerKey = inbound.getIdentifiedKey();
if (!playerKey.verifyDataSignature(packet.getVerifyToken(), verify,
Longs.toByteArray(packet.getSalt()))) {
throw new IllegalStateException("Invalid client public signature.");
}
} else {
byte[] decryptedVerifyToken = decryptRsa(serverKeyPair, packet.getVerifyToken());
if (!MessageDigest.isEqual(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
}
}
final byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
final String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
final String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
JsonObject data = new JsonObject();
data.addProperty("username", login.getUsername());
data.addProperty("serverId", serverId);
data.addProperty("gameID", NeteaseServerConfig.getGameId());
HttpRequest httpRequest = HttpRequest.newBuilder()
.headers("Content-Type", "application/json")
.uri(URI.create(NeteaseServerConfig.getAuthUrl()))
.POST(HttpRequest.BodyPublishers.ofString(data.toString()))
.build();
@SuppressWarnings("resource")
final HttpClient httpClient = server.createHttpClient();
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.whenCompleteAsync((response, throwable) -> {
if (mcConnection.isClosed()) {
// The player disconnected after we authenticated them.
return;
}
if (throwable != null) {
logger.error("Unable to authenticate player", throwable);
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
return;
}
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
// is enabled.
try {
mcConnection.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption for connection", e);
// At this point, the connection is encrypted, but something's wrong on our side and
// we can't do anything about it.
mcConnection.close(true);
return;
}
try {
if (response.statusCode() == 200) {
final NeteaseAuthResponse authResponse = GENERAL_GSON.fromJson(response.body(), NeteaseAuthResponse.class);
if (authResponse.getCode() != 0) {
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
logger.error("Error authenticating {} with netease", login.getUsername());
} else {
NeteaseAuthResponse.ResponseEntity entity = authResponse.getEntity();
if (entity.getName() == null || entity.getName().isEmpty()) {
entity.setName(login.getUsername());
}
if (entity.getProperties() == null) {
entity.setProperties(new ArrayList<>());
}
if (entity.getId() == null) {
inbound.disconnect(
Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED)
);
} else {
GameProfile profile = new GameProfile(entity.getId(), entity.getName(), entity.getProperties());
// Not so fast, now we verify the public key for 1.19.1+
if (inbound.getIdentifiedKey() != null
&& inbound.getIdentifiedKey().getKeyRevision() == IdentifiedKey.Revision.LINKED_V2
&& inbound.getIdentifiedKey() instanceof final IdentifiedKeyImpl key) {
if (!key.internalAddHolder(profile.getId())) {
inbound.disconnect(
Component.translatable("multiplayer.disconnect.invalid_public_key"));
}
}
// All went well, initialize the session.
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
new AuthSessionHandler(server, inbound, profile, true));
}
}
} else if (response.statusCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect(
Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED));
} else {
// Something else went wrong
logger.error(
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
response.statusCode(), login.getUsername(), playerIp);
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
}
} catch (Exception e) {
logger.error("Got an unexpected error", e);
}
}, mcConnection.eventLoop())
.thenRun(() -> {
//noinspection ConstantValue
if (httpClient instanceof final AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception e) {
// In Java 21, the HttpClient does not throw any Exception
// when trying to clean its resources, so this should not happen
logger.error("An unknown error occurred while trying to close an HttpClient", e);
}
}
});
} catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e);
mcConnection.close(true);
}
cir.cancel();
cir.setReturnValue(true);
}
}

View File

@@ -0,0 +1,10 @@
{
"id": "velocity-netease-auth",
"version": "${version}",
"mixins": [
"mixins.velocity-netease-auth.json"
],
"wideners": [
"velocity-netease-auth.accesswidener"
]
}

View File

@@ -0,0 +1,12 @@
{
"required": true,
"package": "cn.hamster3.mc.auth.mixin",
"compatibilityLevel": "JAVA_21",
"minVersion": "0.8",
"mixins": [
"InitialLoginSessionHandlerMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@@ -0,0 +1,14 @@
# 正式环境http://x19authserver.nie.netease.com/check
# 测试环境http://x19authexpr.nie.netease.com/check
#
# 1.20版本请使用以下接口:
# 正式环境https://x19apigatewayobt.nie.netease.com/pcauth/check
# 测试环境https://x19apigatewayexpr.nie.netease.com/pcauth/check
#
# 另有外网测试认证接口:
# http://x19authtest.nie.netease.com/check
# 对应接入 test 版 bc 认证,通常情况下不使用,需要启用时会另行沟通
auth-url: http://192.168.0.100:9999/check
# 网络服游戏 id在开发者平台中可以查看
game-id: 12345678901234567

View File

@@ -0,0 +1,3 @@
accessWidener v2 named
accessible class com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler$LoginState
accessible method com/velocitypowered/proxy/connection/client/AuthSessionHandler <init> (Lcom/velocitypowered/proxy/VelocityServer;Lcom/velocitypowered/proxy/connection/client/LoginInboundConnection;Lcom/velocitypowered/api/util/GameProfile;Z)V