Skip to content

Commit

Permalink
Merge pull request #4 from YuxuanZuo/develop
Browse files Browse the repository at this point in the history
Release v0.1.3
  • Loading branch information
YuxuanZuo authored Jul 5, 2022
2 parents 0fd296a + 6987ddb commit 0275a7f
Show file tree
Hide file tree
Showing 17 changed files with 204 additions and 59 deletions.
14 changes: 14 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -659,3 +659,17 @@ specific requirements.
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.


"MULTIYGGDRASIL" EXCEPTION TO THE AGPL

As a special exception, using this work in the following ways does not cause
your program to be covered by the AGPL:

a) Bundling the unaltered binary form of this work in your program without
statically or dynamically linking to it; or

b) Interacting with this work through the provided inter-process
communication interface, such as the HTTP API; or

c) Loading this work as a Java Agent into a Java Virtual Machine.
23 changes: 21 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,15 @@ Configure Minecraft server with the following JVM parameter:
-Dauthlibinjector.profileKey={default|enabled|disabled}
Whether to enable the profile signing key feature. This feature is introduced in 22w17a, and is used to implement the multiplayer secure chat signing.
If this this feature is enabled, Minecraft will send a POST request to /minecraftservices/player/certificates to retrieve the key pair issued by the authentication server.
It's enabled by default if the authentication server sends feature.enable_profile_key option.
It's disabled by default if the authentication server does NOT send feature.enable_profile_key option.
If the profile signing key isn't present, the player will be unable to join servers that enable enforce-secure-profile=true option.
And other players' Minecraft client will log a warning when receiving an unsigned chat message.
And other players' Minecraft client will log a warning message when receiving an unsigned chat message.
-Dauthlibinjector.usernameCheck={default|enabled|disabled}
Whether to enable username validation. If disabled, Minecraft, BungeeCord and Paper will NOT perform username validation.
It's disabled by default if the authentication server does NOT send feature.usernameCheck option.
Turning on this option will prevent players whose username contains special characters from joining the server.
-Dmultiyggdrasil.mojangYggdrasilService={default|enabled|disabled}
Whether to enable ability to coexist with the Mojang authentication server.
Expand All @@ -127,6 +132,10 @@ Configure Minecraft server with the following JVM parameter:
Some features that conflict with Mojang Yggdrasil server will no longer available anymore:
- Mojang namespace
-Dmultiyggdrasil.priorityVerifyingCustomName
Make the custom authentication server a priority to verify the player when logging into the game server
(The default is to give priority to verification of the genuine player).
-Dmultiyggdrasil.namespace={namespace string}
Set the namespace used by the feature "Mojang authentication server". Allowed characters are a-z0-9._- .
Expand All @@ -137,6 +146,16 @@ Configure Minecraft server with the following JVM parameter:
This feature can be disabled using this option.
```

## License
This work is licensed under the [GNU Affero General Public License v3.0](https://github.com/YuxuanZuo/MultiYggdrasil/blob/develop/LICENSE) or later, with the "MULTIYGGDRASIL" exception.

> **"MULTIYGGDRASIL" EXCEPTION TO THE AGPL**
>
> As a special exception, using this work in the following ways does not cause your program to be covered by the AGPL:
> 1. Bundling the unaltered binary form of this work in your program without statically or dynamically linking to it; or
> 2. Interacting with this work through the provided inter-process communication interface, such as the HTTP API; or
> 3. Loading this work as a Java Agent into a Java Virtual Machine.
## Credits
* [authlib-injector](https://github.com/yushijinhun/authlib-injector) by [Haowei Wen](https://github.com/yushijinhun)
This is the base of this project, which makes our ideas possible.
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ gradle
当缺少消息签名密钥时, 玩家将无法进入设置了 enforce-secure-profile=true 选项的服务器.
而当其他玩家的客户端在收到无有效签名的聊天消息时, 会在日志中记录警告.
-Dauthlibinjector.usernameCheck={default|enabled|disabled}
是否启用玩家用户名检查, 若禁用, 则 authlib-injector 将关闭 Minecraft、BungeeCord 和 Paper 的用户名检查功能.
若验证服务器未设置 feature.usernameCheck 选项, 则默认禁用.
注意, 开启此功能将导致用户名包含非英文字符的玩家无法进入服务器.
-Dmultiyggdrasil.mojangYggdrasilService={default|enabled|disabled}
设置是否与 Mojang 验证服务器共存.
若验证服务器未设置 feature.enable_mojang_yggdrasil_service 选项, 则默认禁用.
Expand All @@ -128,6 +133,9 @@ gradle
以下与 Mojang 验证服务器冲突的功能将不可用:
- Mojang 命名空间
-Dmultiyggdrasil.priorityVerifyingCustomName
在登录游戏服务器时优先验证来自自定义验证服务器的角色(默认为优先验证正版角色).
-Dmultiyggdrasil.namespace={命名空间字符串}
设置 Mojang 验证服务器 功能使用的命名空间, 允许的字符为 a-z0-9._- .
Expand All @@ -136,6 +144,16 @@ gradle
默认情况下, MultiYggdrasil 会自动在用户名中添加命名空间后缀以允许来自不同验证服务器的角色同时进行游戏, 使用本选项可以禁用该功能.
```

## 许可
本程序使用 [GNU Affero General Public License v3.0 or later](https://github.com/YuxuanZuo/MultiYggdrasil/blob/develop/LICENSE) 许可,并附有以下例外:

> **AGPL 的例外情况:**
>
> 作为特例,如果您的程序通过以下方式利用本作品,则相应的行为不会导致您的作品被 AGPL 协议涵盖。
> 1. 您的程序通过打包的方式包含本作品未经修改的二进制形式,而没有静态或动态地链接到本作品;或
> 2. 您的程序通过本作品提供的进程间通信接口(如 HTTP API)进行交互;或
> 3. 您的程序将本作品作为 Java Agent 加载进 Java 虚拟机。
## Credits
* [authlib-injector](https://github.com/yushijinhun/authlib-injector) by [Haowei Wen](https://github.com/yushijinhun)
这是本项目的基础, 它使得我们的想法成为可能.
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/xyz/zuoyx/multiyggdrasil/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public boolean isEnabled(boolean defaultValue) {
public static boolean httpdDisabled;
public static boolean noShowServerName;
public static boolean noLogFile;
public static boolean priorityVerifyingCustomName;
public static boolean noNamespaceSuffix;
public static int httpdPort;
public static String namespace;
Expand All @@ -61,6 +62,7 @@ public boolean isEnabled(boolean defaultValue) {
public static FeatureOption legacySkinPolyfill;
public static FeatureOption mojangAntiFeatures;
public static FeatureOption profileKey;
public static FeatureOption usernameCheck;

private static void initDebugOptions() {
String prop = System.getProperty("authlibinjector.debug");
Expand Down Expand Up @@ -161,9 +163,11 @@ static void init() {
legacySkinPolyfill = parseFeatureOption("authlibinjector.legacySkinPolyfill");
mojangAntiFeatures = parseFeatureOption("authlibinjector.mojangAntiFeatures");
profileKey = parseFeatureOption("authlibinjector.profileKey");
usernameCheck = parseFeatureOption("authlibinjector.usernameCheck");
httpdDisabled = System.getProperty("authlibinjector.disableHttpd") != null;
noShowServerName = System.getProperty("authlibinjector.noShowServerName") != null;
noLogFile = System.getProperty("authlibinjector.noLogFile") != null;
priorityVerifyingCustomName = System.getProperty("multiyggdrasil.priorityVerifyingCustomName") != null;
noNamespaceSuffix = System.getProperty("multiyggdrasil.noNamespaceSuffix") != null;
httpdPort = Integer.getInteger("authlibinjector.httpdPort", 0);
namespace = System.getProperty("multiyggdrasil.namespace");
Expand Down
45 changes: 29 additions & 16 deletions src/main/java/xyz/zuoyx/multiyggdrasil/MultiYggdrasil.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asBytes;
import static xyz.zuoyx.multiyggdrasil.util.IOUtils.asString;
import static xyz.zuoyx.multiyggdrasil.util.IOUtils.removeNewLines;
import static xyz.zuoyx.multiyggdrasil.util.JsonUtils.asBoolean;
import static xyz.zuoyx.multiyggdrasil.util.Logging.log;
import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.DEBUG;
import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.ERROR;
Expand Down Expand Up @@ -64,6 +65,7 @@
import xyz.zuoyx.multiyggdrasil.transform.support.CitizensTransformer;
import xyz.zuoyx.multiyggdrasil.transform.support.ConstantURLTransformUnit;
import xyz.zuoyx.multiyggdrasil.transform.support.MainArgumentsTransformer;
import xyz.zuoyx.multiyggdrasil.transform.support.PaperUsernameCheckTransformer;
import xyz.zuoyx.multiyggdrasil.transform.support.ProxyParameterWorkaround;
import xyz.zuoyx.multiyggdrasil.transform.support.SkinWhitelistTransformUnit;
import xyz.zuoyx.multiyggdrasil.transform.support.UsernameCharacterCheckTransformer;
Expand Down Expand Up @@ -231,46 +233,50 @@ private static List<URLFilter> createFilters(APIMetadata config) {
YggdrasilClient customClient = new YggdrasilClient(new CustomYggdrasilAPIProvider(config));
YggdrasilClient mojangClient = new YggdrasilClient(new MojangYggdrasilAPIProvider(), Config.mojangProxy);

boolean legacySkinPolyfillDefault = !Boolean.TRUE.equals(ofNullable(config.getMeta().get("feature.legacy_skin_api"))
.map(element -> element.getAsJsonPrimitive().getAsBoolean())
.orElse(Boolean.FALSE));
boolean legacySkinPolyfillDefault = !Boolean.TRUE.equals(asBoolean(config.getMeta().get("feature.legacy_skin_api")));
if (Config.legacySkinPolyfill.isEnabled(legacySkinPolyfillDefault)) {
filters.add(new LegacySkinAPIFilter(customClient));
} else {
log(INFO, "Disabled legacy skin API polyfill");
}

boolean mojangYggdrasilServiceDefault = Boolean.TRUE.equals(ofNullable(config.getMeta().get("feature.enable_mojang_yggdrasil_service"))
.map(element -> element.getAsJsonPrimitive().getAsBoolean())
.orElse(Boolean.FALSE));
boolean mojangYggdrasilServiceDefault = Boolean.TRUE.equals(asBoolean(config.getMeta().get("feature.enable_mojang_yggdrasil_service")));
if (Config.mojangYggdrasilService.isEnabled(mojangYggdrasilServiceDefault)) {
log(INFO, "Mojang Yggdrasil service is enabled, Mojang namespace will be disabled!");
String namespace = getNamespace(config);
filters.add(new MultiHasJoinedServerFilter(mojangClient, customClient, namespace));

YggdrasilClient[] clients;
if (Config.priorityVerifyingCustomName) {
clients = new YggdrasilClient[]{customClient, mojangClient};
} else {
clients = new YggdrasilClient[]{mojangClient, customClient};
}

if (Config.noNamespaceSuffix) {
filters.add(new MultiHasJoinedServerFilter(clients));
} else {
filters.add(new MultiHasJoinedServerFilter(clients, namespace));
}
filters.add(new MultiQueryUUIDsFilter(mojangClient, customClient, namespace));
filters.add(new MultiQueryProfileFilter(mojangClient, customClient, namespace));
} else {
log(INFO, "Disabled Mojang Yggdrasil service");
}

boolean mojangNamespaceDefault = !Boolean.TRUE.equals(ofNullable(config.getMeta().get("feature.no_mojang_namespace"))
.map(element -> element.getAsJsonPrimitive().getAsBoolean())
.orElse(Boolean.FALSE));
boolean mojangNamespaceDefault = !Boolean.TRUE.equals(asBoolean(config.getMeta().get("feature.no_mojang_namespace")));
if (Config.mojangNamespace.isEnabled(mojangNamespaceDefault) && !Config.mojangYggdrasilService.isEnabled(mojangYggdrasilServiceDefault)) {
filters.add(new QueryUUIDsFilter(mojangClient, customClient));
filters.add(new QueryProfileFilter(mojangClient, customClient));
} else {
log(INFO, "Disabled Mojang namespace");
}

boolean mojangAntiFeaturesDefault = Boolean.TRUE.equals(ofNullable(config.getMeta().get("feature.enable_mojang_anti_features"))
.map(element -> element.getAsJsonPrimitive().getAsBoolean())
.orElse(Boolean.FALSE));
boolean mojangAntiFeaturesDefault = Boolean.TRUE.equals(asBoolean(config.getMeta().get("feature.enable_mojang_anti_features")));
if (!Config.mojangAntiFeatures.isEnabled(mojangAntiFeaturesDefault)) {
filters.add(new AntiFeaturesFilter());
}

boolean profileKeyDefault = Boolean.TRUE.equals(config.getMeta().get("feature.enable_profile_key"));
boolean profileKeyDefault = Boolean.TRUE.equals(asBoolean(config.getMeta().get("feature.enable_profile_key")));
if (!Config.profileKey.isEnabled(profileKeyDefault)) {
filters.add(new ProfileKeyFilter());
}
Expand All @@ -295,11 +301,18 @@ private static ClassTransformer createTransformer(APIMetadata config) {
transformer.units.add(new MainArgumentsTransformer());
transformer.units.add(new ConstantURLTransformUnit(urlProcessor));
transformer.units.add(new CitizensTransformer());
transformer.units.add(new BungeeCordAllowedCharactersTransformer());
transformer.units.add(new UsernameCharacterCheckTransformer());
transformer.units.add(new HasJoinedServerTransformer());
transformer.units.add(new HasJoinedServerResponseTransformer());

boolean usernameCheckDefault = Boolean.TRUE.equals(asBoolean(config.getMeta().get("feature.username_check")));
if (Config.usernameCheck.isEnabled(usernameCheckDefault)) {
log(INFO, "Username check is enforced");
} else {
transformer.units.add(new BungeeCordAllowedCharactersTransformer());
transformer.units.add(new UsernameCharacterCheckTransformer());
transformer.units.add(new PaperUsernameCheckTransformer());
}

transformer.units.add(new SkinWhitelistTransformUnit());
SkinWhitelistTransformUnit.getWhitelistedDomains().addAll(config.getSkinDomains());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,32 @@
import static xyz.zuoyx.multiyggdrasil.util.Logging.Level.ERROR;
import static xyz.zuoyx.multiyggdrasil.util.Logging.log;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

import xyz.zuoyx.multiyggdrasil.Config;
import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession;
import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response;
import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status;
import xyz.zuoyx.multiyggdrasil.yggdrasil.GameProfile;
import xyz.zuoyx.multiyggdrasil.yggdrasil.NamespacedID;
import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilAPIProvider;
import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilClient;
import xyz.zuoyx.multiyggdrasil.yggdrasil.MojangYggdrasilAPIProvider;
import xyz.zuoyx.multiyggdrasil.yggdrasil.YggdrasilResponseBuilder;

public class MultiHasJoinedServerFilter implements URLFilter {

private YggdrasilClient mojangClient;
private YggdrasilClient customClient;
private YggdrasilClient[] clients;
private String namespace;

public MultiHasJoinedServerFilter(YggdrasilClient mojangClient, YggdrasilClient customClient, String namespace) {
this.mojangClient = mojangClient;
this.customClient = customClient;
public MultiHasJoinedServerFilter(YggdrasilClient[] clients) {
this.clients = clients;
}

public MultiHasJoinedServerFilter(YggdrasilClient[] clients, String namespace) {
this.clients = clients;
this.namespace = namespace;
}

Expand All @@ -53,24 +55,28 @@ public boolean canHandle(String domain) {
}

@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
public Optional<Response> handle(String domain, String path, IHTTPSession session) {
if (domain.equals("sessionserver.mojang.com") && path.equals("/session/minecraft/hasJoined") && session.getMethod().equals("GET")) {
Map<String, String> params = new LinkedHashMap<>();
session.getParameters().forEach(
(key ,value) -> params.put(key, value.get(0))
);

Optional<GameProfile> response = Optional.empty();
try {
response = mojangClient.hasJoinedServer(params.get("username"), params.get("serverId"), params.get("ip"));
} catch (UncheckedIOException e) {
log(ERROR, "An error occurred while verifying username [ " + params.get("username") + " ] at Mojang Yggdrasil server:\n" +
e.getCause());
}
if (response.isEmpty()) {
response = customClient.hasJoinedServer(params.get("username"), params.get("serverId"), params.get("ip"));
if (!Config.noNamespaceSuffix) {
response.ifPresent(profile -> profile.name = new NamespacedID(profile.name, namespace).toString());
for (YggdrasilClient client : clients) {
YggdrasilAPIProvider apiProvider = client.getApiProvider();
try {
response = client.hasJoinedServer(params.get("username"), params.get("serverId"), params.get("ip"));
} catch (UncheckedIOException e) {
log(ERROR, "An error occurred while verifying username [ " + params.get("username") + " ] at [ " + apiProvider + " ]:\n" +
e.getCause());
continue;
}
if (response.isPresent()) {
if (namespace != null && !(apiProvider instanceof MojangYggdrasilAPIProvider)) {
response.ifPresent(profile -> profile.name = new NamespacedID(profile.name, namespace).toString());
}
break;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import static xyz.zuoyx.multiyggdrasil.util.IOUtils.CONTENT_TYPE_JSON;
import static xyz.zuoyx.multiyggdrasil.util.UUIDUtils.fromUnsignedUUID;

import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
Expand Down Expand Up @@ -55,7 +54,7 @@ public boolean canHandle(String domain) {
}

@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
public Optional<Response> handle(String domain, String path, IHTTPSession session) {
if (!domain.equals("sessionserver.mojang.com"))
return empty();
Matcher matcher = PATH_REGEX.matcher(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@
*/
package xyz.zuoyx.multiyggdrasil.httpd;

import java.io.IOException;
import java.util.Optional;
import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.IHTTPSession;
import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Response;
import xyz.zuoyx.multiyggdrasil.internal.fi.iki.elonen.Status;

/**
* Intercepts Minecraft's request to https://api.minecraftservices.com/player/certificates,
* Intercepts Minecraft's request to <a href="https://api.minecraftservices.com/player/certificates">...</a>,
* and returns an empty response.
*/
public class ProfileKeyFilter implements URLFilter {
Expand All @@ -34,7 +33,7 @@ public boolean canHandle(String domain) {
}

@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
public Optional<Response> handle(String domain, String path, IHTTPSession session) {
if (domain.equals("api.minecraftservices.com") && path.equals("/player/certificates") && session.getMethod().equals("POST")) {
return Optional.of(Response.newFixedLength(Status.NO_CONTENT, null, null));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
/**
* Output stream that will automatically send every write to the wrapped
* OutputStream according to chunked transfer:
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1">...</a>
*/
class ChunkedOutputStream extends FilterOutputStream {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public void accept(TransformUnit... units) {
for (int i = units.length - 1; i >= 0; i--) {
TransformContextImpl ctx = new TransformContextImpl();
Optional<ClassVisitor> visitor = units[i].transform(classLoader, className, chain, ctx);
if (!visitor.isPresent())
if (visitor.isEmpty())
continue;
ctxs[i] = ctx;
chain = visitor.get();
Expand Down
Loading

0 comments on commit 0275a7f

Please sign in to comment.