Compare commits

...

2 Commits

Author SHA1 Message Date
56c7d0d753 Merge branch 'dev/3.0.0' into netease/dev
Some checks failed
Java CI with Gradle / build (push) Failing after 6m42s
# Conflicts:
#	api/src/main/java/com/velocitypowered/api/proxy/config/ProxyConfig.java
#	proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
2025-07-24 09:19:06 +08:00
7cd3011e45 feat: 魔改网易登录接口
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-01-28 19:20:32 +08:00
6 changed files with 222 additions and 41 deletions

View File

@@ -5,7 +5,7 @@ plugins {
} }
java { java {
withJavadocJar() // withJavadocJar()
withSourcesJar() withSourcesJar()
sourceSets["main"].java { sourceSets["main"].java {

View File

@@ -203,4 +203,8 @@ public interface ProxyConfig {
default boolean isKickOnTabCompleteRateLimit() { default boolean isKickOnTabCompleteRateLimit() {
return getKickAfterRateLimitedTabCompletes() > 0; return getKickAfterRateLimitedTabCompletes() > 0;
} }
String getNeteaseAuthUrl();
String getNeteaseGameId();
} }

View File

@@ -19,7 +19,7 @@ public final class GameProfile {
private final UUID id; private final UUID id;
private final String undashedId; private final String undashedId;
private final String name; private String name;
private final List<Property> properties; private final List<Property> properties;
/** /**
@@ -80,6 +80,10 @@ public final class GameProfile {
return name; return name;
} }
public void setName(String name) {
this.name = name;
}
/** /**
* Returns an immutable list of profile properties associated with this profile. * Returns an immutable list of profile properties associated with this profile.
* *
@@ -221,4 +225,116 @@ public final class GameProfile {
+ '}'; + '}';
} }
} }
/**
* netease auth response.
*/
public static class Response {
private Integer code = 0;
private String message;
private String details;
private ResponseEntity entity;
/**
* default constructor.
*
* @param code -
* @param message -
* @param details -
* @param entity the game profile
*/
public Response(Integer code, String message, String details, ResponseEntity entity) {
this.code = code;
this.message = message;
this.details = details;
this.entity = entity;
}
public Integer getCode() {
return code;
}
public void setCode(Integer 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;
}
}
/**
* netease auth response entity.
*/
public static class ResponseEntity {
private String id;
private String name;
private List<Property> properties;
/**
* default constructor.
*
* @param id -
* @param name -
* @param properties -
*/
public ResponseEntity(String id, String name, List<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<Property> getProperties() {
return properties;
}
public void setProperties(List<Property> properties) {
this.properties = properties;
}
@Override
public String toString() {
return "ResponseEntity{"
+ "id='" + id + '\''
+ ", name='" + name + '\''
+ ", properties=" + properties
+ '}';
}
}
} }

View File

@@ -94,6 +94,11 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose @Expose
private boolean forceKeyAuthentication = true; // Added in 1.19 private boolean forceKeyAuthentication = true; // Added in 1.19
@Expose
private String authUrl = "http://192.168.46.50:9999/check";
@Expose
private String gameId = "77140593557373952";
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced, private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
Query query, Metrics metrics) { Query query, Metrics metrics) {
this.servers = servers; this.servers = servers;
@@ -109,7 +114,7 @@ public class VelocityConfiguration implements ProxyConfig {
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers, boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics, ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
boolean forceKeyAuthentication) { boolean forceKeyAuthentication, String authUrl, String gameId) {
this.bind = bind; this.bind = bind;
this.motd = motd; this.motd = motd;
this.showMaxPlayers = showMaxPlayers; this.showMaxPlayers = showMaxPlayers;
@@ -128,6 +133,8 @@ public class VelocityConfiguration implements ProxyConfig {
this.query = query; this.query = query;
this.metrics = metrics; this.metrics = metrics;
this.forceKeyAuthentication = forceKeyAuthentication; this.forceKeyAuthentication = forceKeyAuthentication;
this.authUrl = authUrl;
this.gameId = gameId;
} }
/** /**
@@ -449,6 +456,16 @@ public class VelocityConfiguration implements ProxyConfig {
return advanced.isEnableReusePort(); return advanced.isEnableReusePort();
} }
@Override
public String getNeteaseAuthUrl() {
return authUrl;
}
@Override
public String getNeteaseGameId() {
return gameId;
}
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
@@ -466,6 +483,8 @@ public class VelocityConfiguration implements ProxyConfig {
.add("favicon", favicon) .add("favicon", favicon)
.add("enablePlayerAddressLogging", enablePlayerAddressLogging) .add("enablePlayerAddressLogging", enablePlayerAddressLogging)
.add("forceKeyAuthentication", forceKeyAuthentication) .add("forceKeyAuthentication", forceKeyAuthentication)
.add("authUrl", authUrl)
.add("gameId", gameId)
.toString(); .toString();
} }
@@ -565,6 +584,8 @@ public class VelocityConfiguration implements ProxyConfig {
|| forwardingMode == PlayerInfoForwarding.BUNGEEGUARD)) { || forwardingMode == PlayerInfoForwarding.BUNGEEGUARD)) {
throw new RuntimeException("The forwarding-secret file must not be empty."); throw new RuntimeException("The forwarding-secret file must not be empty.");
} }
final String authUrl = config.getOrElse("auth-url", "http://192.168.46.50:9999/check");
final String gameId = config.getOrElse("game-id", "77140593557373952");
return new VelocityConfiguration( return new VelocityConfiguration(
bind, bind,
@@ -584,7 +605,9 @@ public class VelocityConfiguration implements ProxyConfig {
new Advanced(advancedConfig), new Advanced(advancedConfig),
new Query(queryConfig), new Query(queryConfig),
new Metrics(metricsConfig), new Metrics(metricsConfig),
forceKeyAuthentication forceKeyAuthentication,
authUrl,
gameId
); );
} }
} }

View File

@@ -17,7 +17,6 @@
package com.velocitypowered.proxy.connection.client; package com.velocitypowered.proxy.connection.client;
import static com.google.common.net.UrlEscapers.urlFormParameterEscaper;
import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON; import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY; import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa; import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa;
@@ -25,6 +24,7 @@ import static com.velocitypowered.proxy.crypto.EncryptionUtils.generateServerId;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import com.google.gson.JsonObject;
import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
@@ -50,6 +50,7 @@ import java.net.http.HttpResponse;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
@@ -198,22 +199,23 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
} }
byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret()); final byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic()); final String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
final String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString(); JsonObject data = new JsonObject();
String url = String.format(MOJANG_HASJOINED_URL, data.addProperty("username", login.getUsername());
urlFormParameterEscaper().escape(login.getUsername()), serverId); data.addProperty("serverId", serverId);
data.addProperty("gameID", server.getConfiguration().getNeteaseGameId());
if (server.getConfiguration().shouldPreventClientProxyConnections()) { HttpRequest httpRequest = HttpRequest.newBuilder()
url += "&ip=" + urlFormParameterEscaper().escape(playerIp); .headers(
} "User-Agent", server.getVersion().getName() + "/" + server.getVersion().getVersion(),
"Content-Type", "application/json"
final HttpRequest httpRequest = HttpRequest.newBuilder() ).POST(HttpRequest.BodyPublishers.ofString(data.toString()))
.setHeader("User-Agent", .uri(URI.create(server.getConfiguration().getNeteaseAuthUrl()))
server.getVersion().getName() + "/" + server.getVersion().getVersion())
.uri(URI.create(url))
.build(); .build();
final HttpClient httpClient = server.createHttpClient(); final HttpClient httpClient = server.createHttpClient();
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.whenCompleteAsync((response, throwable) -> { .whenCompleteAsync((response, throwable) -> {
@@ -240,31 +242,53 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
return; return;
} }
if (response.statusCode() == 200) { try {
final GameProfile profile = GENERAL_GSON.fromJson(response.body(), if (response.statusCode() == 200) {
GameProfile.class); final GameProfile.Response authResponse = GENERAL_GSON.fromJson(response.body(),
// Not so fast, now we verify the public key for 1.19.1+ GameProfile.Response.class);
if (inbound.getIdentifiedKey() != null if (authResponse.getCode() != 0) {
&& inbound.getIdentifiedKey().getKeyRevision() == IdentifiedKey.Revision.LINKED_V2 inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
&& inbound.getIdentifiedKey() instanceof final IdentifiedKeyImpl key) { logger.error("Error authenticating {} with netease", login.getUsername());
if (!key.internalAddHolder(profile.getId())) { } else {
inbound.disconnect( GameProfile.ResponseEntity entity = authResponse.getEntity();
Component.translatable("multiplayer.disconnect.invalid_public_key")); 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"));
} }
// All went well, initialize the session. } catch (Exception e) {
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, logger.error("Got an unexpected error", e);
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"));
} }
}, mcConnection.eventLoop()) }, mcConnection.eventLoop())
.thenRun(() -> { .thenRun(() -> {

View File

@@ -74,6 +74,20 @@ sample-players-in-ping = false
# If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs # If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs
enable-player-address-logging = true enable-player-address-logging = true
# 认证链接
# 正式环境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
# 另有外网测试认证接口对应接入test版bc认证通常情况下不使用需要启用时会另行沟通。
# http://x19authtest.nie.netease.com/check
auth-url = "http://192.168.46.50:9999/check"
# 网络服游戏 id
# 在开发者平台中可以查看
game-id = "77140593557373952"
[servers] [servers]
# Configure your servers here. Each key represents the server's name, and the value # Configure your servers here. Each key represents the server's name, and the value
# represents the IP address of the server to connect to. # represents the IP address of the server to connect to.