Compare commits

...

67 commits

Author SHA1 Message Date
Haowei Wen cf9248bdbf Fix not working on Velocity (#234) 2024-02-18 01:13:27 +08:00
Haowei Wen 18d708d62c Fix UUID lookup is not working on MC 1.20.4+ (#232) 2024-02-18 00:31:20 +08:00
Haowei Wen da910956ea Revert "[ci] trigger deploy_release on edited"
This reverts commit 0fe9beb63b.
2023-11-01 19:50:40 +08:00
Haowei Wen 0fe9beb63b [ci] trigger deploy_release on edited 2023-11-01 19:41:16 +08:00
Haowei Wen c7670af3d4
Merge pull request #222 from unmojang/evan-goode/release-date
Include release_time in release JSON manifest
2023-11-01 19:08:56 +08:00
Haowei Wen 088af75652 Update dependencies 2023-11-01 18:52:13 +08:00
Haowei Wen f6970a51b2 Fix skin not displayed in Minecraft 1.20.2 2023-11-01 18:47:45 +08:00
Evan Goode 2b90fd9816 Include release_time in release JSON manifest
For https://github.com/yushijinhun/authlib-injector/issues/219
2023-09-23 17:51:33 -04:00
Haowei Wen acef784f82
Merge pull request #221 from Glavo/develop
Bump ASM from 9.4 to 9.5
2023-09-21 21:02:56 +08:00
Glavo df8299b8e7
Bump ASM from 9.4 to 9.5 2023-09-21 20:38:04 +08:00
Haowei Wen b68049208a Fix logging is broken in Minecraft 1.20 2023-06-12 01:49:53 +08:00
Haowei Wen 18a0ce2669 Fix other players' skins are not visible in Minecraft 1.20 2023-06-12 01:14:50 +08:00
Haowei Wen 648464df24 Fix chat is broken in Minecraft 1.20 2023-06-12 00:30:58 +08:00
Haowei Wen 15c89a6fef update dependencies 2023-03-25 18:28:43 +08:00
Haowei Wen 97fbb3e0c0 build with jdk 17 2023-03-25 18:11:11 +08:00
Haowei Wen cda44078a5 update .gitignore 2023-03-25 18:03:57 +08:00
Haowei Wen 5382d5289c fix ci 2023-03-25 17:55:11 +08:00
Haowei Wen 560443013e fix ci 2023-03-25 17:37:52 +08:00
Haowei Wen ef2b0e418a fix readme badges 2023-03-25 17:15:50 +08:00
Haowei Wen d767720c14
Merge pull request #203 from ARTI5T/develop
Fix: authlib 3.18.38 support
2023-03-25 17:13:15 +08:00
ARTI5T 9f53978332
Fix: authlib 3.18.38 support
Transforms com.mojang.authlib.yggdrasil.TextureUrlChecker in SkinWhitelistTransformUnit.
2023-03-25 16:25:27 +08:00
Haowei Wen 29d2667000 Support BungeeCord MC 1.19+ 2022-08-06 00:38:31 +08:00
Haowei Wen f2ca302c1a fix compile error 2022-08-06 00:36:02 +08:00
Haowei Wen 9c0f278bb4 Support Velocity MC 1.19+ 2022-08-06 00:19:07 +08:00
Haowei Wen c8882173be Fix 1.19.1 insecure chat warning 2022-08-05 00:21:00 +08:00
Haowei Wen 81cf840291 Refactor signature verifying 2022-08-04 22:27:32 +08:00
Haowei Wen 5059a55789 fix class version below 50 is not supported 2022-07-10 04:25:24 +08:00
Haowei Wen 4862db0572 fix setting feature.username_check doesn't disable BungeeCordAllowedCharactersTransformer 2022-07-09 23:27:21 +08:00
Haowei Wen a132a2964d Add exception to the AGPL
This exception allows launchers to bundle authlib-injector in their program.
2022-07-03 15:30:46 +08:00
Haowei Wen 74b890ff01 polish 2022-07-02 14:00:25 +08:00
Haowei Wen 381ae455af rename feature.usernameCheck -> feature.username_check 2022-07-02 13:28:14 +08:00
Haowei Wen 8fa86e7f93 add -Dauthlibinjector.usernameCheck option 2022-07-02 13:24:51 +08:00
Haowei Wen 942b056c58 disable papermc's username check 2022-07-02 12:53:41 +08:00
Haowei Wen dbfcbf5b8c support authlib 3.5.41, fix #161 2022-06-05 19:10:56 +08:00
Haowei Wen dee8e6989e switch to junit 5 2022-05-03 23:15:27 +08:00
Haowei Wen 0fada946b8 update dependencies 2022-05-03 22:13:49 +08:00
Haowei Wen 9f29e8db80 fix dumpClass doesn't save the final result 2022-05-03 22:08:54 +08:00
Haowei Wen dc35c04474 simplify the ignored packages list 2022-05-03 01:14:55 +08:00
Haowei Wen 45ca1e0f84 optimize url matching 2022-05-03 01:14:00 +08:00
Haowei Wen cfbfc7f5ba more performance metrics 2022-05-03 01:14:00 +08:00
Haowei Wen 287698f4c7 optimize logging 2022-05-02 22:27:56 +08:00
Haowei Wen b6b6f01b08 use List to store constant strings instead of Set 2022-04-30 18:38:50 +08:00
Haowei Wen ab44075cbb improve bytecode analysis performance (~5x faster) 2022-04-30 18:28:09 +08:00
Haowei Wen ed8782ee1c add -Dauthlibinjector.httpdPort={port} option 2022-04-30 16:54:22 +08:00
Haowei Wen e1d504f633 add performance metrics 2022-04-30 16:48:02 +08:00
Haowei Wen 940687e131 add ignored packages 2022-04-30 12:50:28 +08:00
Haowei Wen 2363588275 fix readme 2022-04-30 12:33:13 +08:00
Haowei Wen 2e20898981 fix not working on 22w17a 2022-04-30 12:28:49 +08:00
Haowei Wen bb274fb8bb fix readme 2022-04-30 12:28:40 +08:00
Haowei Wen 565fc90dd7 add authlibinjector.mojangAntiFeatures option 2022-03-20 05:52:54 +08:00
Haowei Wen 8fe84c802e remove username character restriction introduced in 22w06a, close #150 2022-03-16 01:06:54 +08:00
Haowei Wen a55936fbf4 Fix BungeeCord allowed characters problem 2022-03-11 13:09:39 +08:00
Haowei Wen 2a1b1ce7f7 filter /player/attributes, /privacy/blocklist, /blockedservers APIs 2021-11-18 21:33:29 +08:00
Haowei Wen a26279da80 fix crash when inserting method to interfaces 2021-11-18 21:00:58 +08:00
Haowei Wen a054b8bdc4 add feedback.minecraft.net to blacklisted domains 2021-11-18 19:40:33 +08:00
Haowei Wen 399cb7bda5
update dependencies 2021-08-21 04:16:33 +08:00
Haowei Wen 7f9155580b
support indify string concatenation (jep 280), fix #130 2021-08-21 04:10:09 +08:00
Haowei Wen 07a55b76c4
workflow: change commit author to github-actions[bot] 2021-07-25 03:09:32 +08:00
Haowei Wen 8e52c37ecc
fix #126 2021-06-14 05:56:38 +08:00
Haowei Wen 07c4e252dc
remove license-header.txt file 2021-06-13 02:59:25 +08:00
Haowei Wen 8bbac540ba
fix: httpd: duplicated content-length header (#108) 2021-06-12 03:12:23 +08:00
Haowei Wen a2b994e7a2
Merge pull request #124 from yushijinhun/social-interactions
Intercept requests to api.minecraftservices.com
2021-06-12 00:34:58 +08:00
Haowei Wen 3e1b4eb4b3
add telemetry=false to PrivilegesFilter 2021-06-12 00:12:14 +08:00
Haowei Wen 90c4a649f6
Merge branch 'develop' into social-interactions 2021-06-12 00:09:05 +08:00
Haowei Wen 42640b4330
fix #121 2021-06-11 18:29:26 +08:00
Haowei Wen 35b8b58e1d
polyfill api.minecraftservices.com/privileges 2020-12-25 05:50:29 +08:00
Haowei Wen 961366e012
redirect api.minecraftservices.com 2020-12-25 05:20:52 +08:00
50 changed files with 2132 additions and 461 deletions

View file

@ -11,20 +11,22 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Setup JDK 8
uses: actions/setup-java@v1
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: 8
distribution: temurin
java-version: 17
cache: gradle
- name: Build
run: gradle
run: ./gradlew
- name: Test
run: gradle test
run: ./gradlew test
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: build/libs/*

View file

@ -66,14 +66,16 @@ jobs:
Build number: ${{ steps.parse_pr.outputs.build_number }}
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
ref: ${{ steps.parse_pr.outputs.commit }}
- name: Setup JDK 8
uses: actions/setup-java@v1
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: 8
distribution: temurin
java-version: 17
cache: gradle
- id: build
name: Build
@ -81,8 +83,8 @@ jobs:
run: |
export AI_BUILD_NUMBER=${{ steps.parse_pr.outputs.build_number }}
export AI_VERSION_NUMBER=${{ steps.parse_pr.outputs.version_number }}
gradle
gradle test
./gradlew
./gradlew test
asset_path=$(echo build/libs/*.jar)
echo "Build output is at $asset_path"
echo "::set-output name=asset_path::$asset_path"

View file

@ -38,10 +38,11 @@ jobs:
build_number=$(grep -Pom1 '@@release\.build_number=\K.*(?=@@)' <<< $release_body)
version_number=$(grep -Pom1 '@@release\.version_number=\K.*(?=@@)' <<< $release_body)
asset_name='${{ github.event.release.assets[0].name }}'
release_published_at='${{ github.event.release.published_at }}'
cd ~/deploy
git config --local user.name "authlib-injector Deploy Bot"
git config --local user.email "authlib-injector-deploy-bot@yushi.moe"
git config --local user.name "github-actions[bot]"
git config --local user.email "github-actions[bot]@users.noreply.github.com"
mkdir -p "artifact/$build_number"
wget -O "artifact/$build_number/$asset_name" '${{ github.event.release.assets[0].browser_download_url }}'
@ -49,12 +50,14 @@ jobs:
jq -n \
--arg build_number "$build_number" \
--arg version "$version_number" \
--arg release_time "$release_published_at" \
--arg download_url "https://authlib-injector.yushi.moe/artifact/$build_number/$asset_name" \
--arg sha256 "$sha256" \
'
{
"build_number": $build_number|tonumber,
"version": $version,
"release_time": $release_time,
"download_url": $download_url,
"checksums": {
"sha256": $sha256

217
.gitignore vendored
View file

@ -1,54 +1,197 @@
## General housekeeping
# Created by https://www.toptal.com/developers/gitignore/api/vim,java,gradle,eclipse,visualstudiocode,macos
# Edit at https://www.toptal.com/developers/gitignore?templates=vim,java,gradle,eclipse,visualstudiocode,macos
### Eclipse ###
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# CDT- autotools
.autotools
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
# Annotation Processing
.apt_generated/
.apt_generated_test/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
# Uncomment this line if you wish to ignore the project description file.
# Typically, this file would be tracked if it contains build/dependency configurations:
#.project
### Eclipse Patch ###
# Spring Boot Tooling
.sts4-cache/
### Java ###
# Compiled class file
*.class
*.iml
*.ipr
*.iws
# Log file
*.log
*.jar
*.war
*.ear
.cache
libs/
natives/
logs/
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# virtual machine crash logs
hs_err_pid*
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
## Because Macs
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
## Gradle
.gradle/
build/
gradle/
gradlew
out/
output/
run/
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
## sublime
*.sublime*
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
## eclipse
.classpath
.metadata/
.project
.settings/
bin/
eclipse/
### macOS Patch ###
# iCloud generated files
*.icloud
## Intellij IDEA
.idea/
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
## netbeans
nbbuild/
nbproject/
# Session
Session.vim
Sessionx.vim
# vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Gradle ###
.gradle
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties
# Cache of project
.gradletasknamecache
# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
### Gradle Patch ###
# Java heap dump
*.hprof
# End of https://www.toptal.com/developers/gitignore/api/vim,java,gradle,eclipse,visualstudiocode,macos

14
LICENSE
View file

@ -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/>.
"AUTHLIB-INJECTOR" 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.

View file

@ -3,7 +3,7 @@
# authlib-injector
[![latest release](https://img.shields.io/github/v/tag/yushijinhun/authlib-injector?color=yellow&include_prereleases&label=version&sort=semver&style=flat-square)](https://github.com/yushijinhun/authlib-injector/releases)
[![ci status](https://img.shields.io/github/workflow/status/yushijinhun/authlib-injector/CI?style=flat-square)](https://github.com/yushijinhun/authlib-injector/actions?query=workflow%3ACI)
[![ci status](https://img.shields.io/github/actions/workflow/status/yushijinhun/authlib-injector/ci.yml?branch=develop)](https://github.com/yushijinhun/authlib-injector/actions?query=workflow%3ACI)
[![license agpl-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue.svg?style=flat-square)](https://github.com/yushijinhun/authlib-injector/blob/develop/LICENSE)
authlib-injector enables you to build a Minecraft authentication system offering all the features that genuine Minecraft has.
@ -14,7 +14,7 @@ authlib-injector enables you to build a Minecraft authentication system offering
You can download the latest authlib-injector build from [here](https://authlib-injector.yushi.moe/).
## Build
Dependencies: Gradle, JDK 8+
Dependencies: Gradle, JDK 17+. The target Java platform version is 8.
Run:
```
@ -77,8 +77,44 @@ Configure Minecraft server with the following JVM parameter:
- Mojang namespace
- Legacy skin API polyfill
-Dauthlibinjector.httpdPort={port}
Sets the port used by the local HTTP server, defaults to 0 (randomly chosen).
-Dauthlibinjector.noShowServerName
Do not show authentication server name in Minecraft menu screen.
By default, authlib-injector alters --versionType parameter to display the authentication server name.
This feature can be disabled using this option.
-Dauthlibinjector.mojangAntiFeatures={default|enabled|disabled}
Whether to turn on Minecraft's anti-features.
It's disabled by default if the authentication server does NOT send feature.enable_mojang_anti_features option.
These anti-features include:
- Minecraft server blocklist
- The API to query user privileges:
* Online chat (allowed if the option is disabled)
* Multiplayer (allowed if the option is disabled)
* Realms (allowed if the option is disabled)
* Telemetry (turned off if the option is disabled)
* Profanity filter (turned off if the option is disabled)
-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 disabled by default if the authentication server does NOT send feature.enable_profile_key option.
-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.
```
## License
This work is licensed under the [GNU Affero General Public License v3.0](https://github.com/yushijinhun/authlib-injector/blob/develop/LICENSE) or later, with the "AUTHLIB-INJECTOR" exception.
> **"AUTHLIB-INJECTOR" 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.

View file

@ -3,7 +3,7 @@
# authlib-injector
[![latest release](https://img.shields.io/github/v/tag/yushijinhun/authlib-injector?color=yellow&include_prereleases&label=version&sort=semver&style=flat-square)](https://github.com/yushijinhun/authlib-injector/releases)
[![ci status](https://img.shields.io/github/workflow/status/yushijinhun/authlib-injector/CI?style=flat-square)](https://github.com/yushijinhun/authlib-injector/actions?query=workflow%3ACI)
[![ci status](https://img.shields.io/github/actions/workflow/status/yushijinhun/authlib-injector/ci.yml?branch=develop)](https://github.com/yushijinhun/authlib-injector/actions?query=workflow%3ACI)
[![license agpl-3.0](https://img.shields.io/badge/license-AGPL--3.0-blue.svg?style=flat-square)](https://github.com/yushijinhun/authlib-injector/blob/develop/LICENSE)
通过运行时修改 authlib 实现游戏外登录,并为 Yggdrasil 服务端的实现提供规范。
@ -14,7 +14,7 @@
您可以从[这里](https://authlib-injector.yushi.moe/)获取最新的 authlib-injector。
## 构建
构建依赖Gradle、JDK 8+
构建依赖Gradle、JDK 17+(目标 Java 版本为 8
执行以下命令:
```
@ -37,7 +37,7 @@ gradle
需要注意的是, authlib-injector 的日志是不会输出到 Minecraft 服务端/客户端的日志文件中的.
每次启动时日志文件都会被清空. 如果有多个进程使用同一个日志文件, 则只有最早启动的会成功打开日志文件.
每次启动时, 日志文件都会被清空. 如果有多个进程使用同一个日志文件, 则只有最早启动的会成功打开日志文件.
-Dauthlibinjector.mojangNamespace={default|enabled|disabled}
设置是否启用 Mojang 命名空间 (@mojang 后缀).
@ -85,10 +85,46 @@ gradle
- Mojang 命名空间
- 旧式皮肤 API polyfill
-Dauthlibinjector.httpdPort={端口号}
设置内置 HTTP 服务器使用的端口号, 默认为 0 (随机分配).
-Dauthlibinjector.noShowServerName
不要在 Minecraft 主界面展示验证服务器名称.
默认情况下, authlib-injector 通过更改 --versionType 参数来在 Minecraft 主界面显示验证服务器名称, 使用本选项可以禁用该功能.
-Dauthlibinjector.mojangAntiFeatures={default|enabled|disabled}
设置是否开启 Minecraft 的部分 anti-feature.
若验证服务器未设置 feature.enable_mojang_anti_features 选项, 则默认禁用.
Minecraft 的 anti-feature 包括:
- Minecraft 服务器屏蔽列表
- 查询用户权限的接口, 涵盖以下项目:
* 聊天权限 (禁用后默认允许)
* 多人游戏权限 (禁用后默认允许)
* 领域权限 (禁用后默认允许)
* 遥测 (禁用后默认关闭)
* 冒犯性内容过滤 (禁用后默认关闭)
-Dauthlibinjector.profileKey={default|enabled|disabled}
是否启用消息签名密钥对功能, 这一功能在 22w17a 引入, 用于多人游戏中聊天消息的数字签名.
启用此功能后, Minecraft 会向 /minecraftservices/player/certificates 发送 POST 请求, 以获取由验证服务器颁发的密钥对.
此功能需要验证服务器支持, 若验证服务器未设置 feature.enable_profile_key 选项, 则该功能默认禁用.
-Dauthlibinjector.usernameCheck={default|enabled|disabled}
是否启用玩家用户名检查, 若禁用, 则 authlib-injector 将关闭 Minecraft、BungeeCord 和 Paper 的用户名检查功能.
若验证服务器未设置 feature.usernameCheck 选项, 则默认禁用.
注意, 开启此功能将导致用户名包含非英文字符的玩家无法进入服务器.
```
## 捐助
BMCLAPI 为 authlib-injector 提供了[下载镜像站](https://github.com/yushijinhun/authlib-injector/wiki/%E8%8E%B7%E5%8F%96-authlib-injector#bmclapi-%E9%95%9C%E5%83%8F)。如果您想要支持 authlib-injector 的开发,您可以[捐助 BMCLAPI](https://bmclapidoc.bangbang93.com/)。
## 许可
本程序使用 [GNU Affero General Public License v3.0 or later](https://github.com/yushijinhun/authlib-injector/blob/develop/LICENSE) 许可,并附有以下例外:
> **AGPL 的例外情况:**
>
> 作为特例,如果您的程序通过以下方式利用本作品,则相应的行为不会导致您的作品被 AGPL 协议涵盖。
> 1. 您的程序通过打包的方式包含本作品未经修改的二进制形式,而没有静态或动态地链接到本作品;或
> 2. 您的程序通过本作品提供的进程间通信接口(如 HTTP API进行交互
> 3. 您的程序将本作品作为 Java Agent 加载进 Java 虚拟机。

View file

@ -1,6 +1,6 @@
plugins {
id 'com.github.johnrengelman.shadow' version '6.1.0'
id 'com.palantir.git-version' version '0.12.3'
id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'com.palantir.git-version' version '3.0.0'
id 'java'
}
@ -9,11 +9,14 @@ repositories {
}
dependencies {
implementation 'org.ow2.asm:asm:9.1'
testImplementation 'junit:junit:4.13.2'
implementation 'org.ow2.asm:asm:9.6'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
sourceCompatibility = 8
tasks.withType(JavaCompile) {
options.release = 8
options.deprecation = true
}
def buildNumber = System.getenv('AI_BUILD_NUMBER')
def gitInfo = versionDetails()
@ -46,8 +49,12 @@ processResources {
}
}
test {
useJUnitPlatform()
}
shadowJar {
classifier = null
archiveClassifier.set(null)
exclude 'META-INF/maven/**'
exclude 'module-info.class'

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

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

234
gradlew vendored Executable 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

View file

@ -1,14 +0,0 @@
Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2023 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -42,23 +42,33 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import moe.yushi.authlibinjector.httpd.AntiFeaturesFilter;
import moe.yushi.authlibinjector.httpd.DefaultURLRedirector;
import moe.yushi.authlibinjector.httpd.LegacySkinAPIFilter;
import moe.yushi.authlibinjector.httpd.ProfileKeyFilter;
import moe.yushi.authlibinjector.httpd.PublickeysFilter;
import moe.yushi.authlibinjector.httpd.QueryProfileFilter;
import moe.yushi.authlibinjector.httpd.QueryUUIDsFilter;
import moe.yushi.authlibinjector.httpd.URLFilter;
import moe.yushi.authlibinjector.httpd.URLProcessor;
import moe.yushi.authlibinjector.transform.ClassTransformer;
import moe.yushi.authlibinjector.transform.DumpClassListener;
import moe.yushi.authlibinjector.transform.support.AccountTypeTransformer;
import moe.yushi.authlibinjector.transform.support.AuthServerNameInjector;
import moe.yushi.authlibinjector.transform.support.AuthlibLogInterceptor;
import moe.yushi.authlibinjector.transform.support.BungeeCordAllowedCharactersTransformer;
import moe.yushi.authlibinjector.transform.support.BungeeCordProfileKeyTransformUnit;
import moe.yushi.authlibinjector.transform.support.CitizensTransformer;
import moe.yushi.authlibinjector.transform.support.ConcatenateURLTransformUnit;
import moe.yushi.authlibinjector.transform.support.ConstantURLTransformUnit;
import moe.yushi.authlibinjector.transform.support.MC52974Workaround;
import moe.yushi.authlibinjector.transform.support.MC52974_1710Workaround;
import moe.yushi.authlibinjector.transform.support.MainArgumentsTransformer;
import moe.yushi.authlibinjector.transform.support.PaperUsernameCheckTransformer;
import moe.yushi.authlibinjector.transform.support.ProxyParameterWorkaround;
import moe.yushi.authlibinjector.transform.support.SkinWhitelistTransformUnit;
import moe.yushi.authlibinjector.transform.support.UsernameCharacterCheckTransformer;
import moe.yushi.authlibinjector.transform.support.VelocityProfileKeyTransformUnit;
import moe.yushi.authlibinjector.transform.support.YggdrasilKeyTransformUnit;
import moe.yushi.authlibinjector.yggdrasil.CustomYggdrasilAPIProvider;
import moe.yushi.authlibinjector.yggdrasil.MojangYggdrasilAPIProvider;
@ -239,6 +249,18 @@ public final class AuthlibInjector {
log(INFO, "Disabled Mojang namespace");
}
boolean mojangAntiFeaturesDefault = Boolean.TRUE.equals(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"));
if (!Config.profileKey.isEnabled(profileKeyDefault)) {
filters.add(new ProfileKeyFilter());
}
filters.add(new PublickeysFilter());
return filters;
}
@ -246,7 +268,7 @@ public final class AuthlibInjector {
URLProcessor urlProcessor = new URLProcessor(createFilters(config), new DefaultURLRedirector(config));
ClassTransformer transformer = new ClassTransformer();
transformer.ignores.addAll(Config.ignoredPackages);
transformer.setIgnores(Config.ignoredPackages);
if (Config.dumpClass) {
transformer.listeners.add(new DumpClassListener(Paths.get("").toAbsolutePath()));
@ -259,12 +281,25 @@ public final class AuthlibInjector {
transformer.units.add(new MainArgumentsTransformer());
transformer.units.add(new ConstantURLTransformUnit(urlProcessor));
transformer.units.add(new CitizensTransformer());
transformer.units.add(new ConcatenateURLTransformUnit());
boolean usernameCheckDefault = Boolean.TRUE.equals(config.getMeta().get("feature.username_check"));
if (Config.usernameCheck.isEnabled(usernameCheckDefault)) {
log(INFO, "Username check is enforced");
} else {
transformer.units.add(new UsernameCharacterCheckTransformer());
transformer.units.add(new PaperUsernameCheckTransformer());
transformer.units.add(new BungeeCordAllowedCharactersTransformer());
}
transformer.units.add(new SkinWhitelistTransformUnit());
SkinWhitelistTransformUnit.getWhitelistedDomains().addAll(config.getSkinDomains());
transformer.units.add(new YggdrasilKeyTransformUnit());
config.getDecodedPublickey().ifPresent(YggdrasilKeyTransformUnit.PUBLIC_KEYS::add);
transformer.units.add(new VelocityProfileKeyTransformUnit());
transformer.units.add(new BungeeCordProfileKeyTransformUnit());
MainArgumentsTransformer.getArgumentsListeners().add(new AccountTypeTransformer()::transform);
return transformer;
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -59,7 +59,11 @@ public final class Config {
public static Set<String> ignoredPackages;
public static FeatureOption mojangNamespace;
public static FeatureOption legacySkinPolyfill;
public static FeatureOption mojangAntiFeatures;
public static FeatureOption profileKey;
public static FeatureOption usernameCheck;
public static boolean noShowServerName;
public static int httpdPort;
private static void initDebugOptions() {
String prop = System.getProperty("authlibinjector.debug");
@ -104,34 +108,6 @@ public final class Config {
"com.sun.",
"sun.",
"net.java.",
"com.google.",
"com.ibm.",
"com.jcraft.jogg.",
"com.jcraft.jorbis.",
"com.oracle.",
"com.paulscode.",
"org.GNOME.",
"org.apache.",
"org.graalvm.",
"org.jcp.",
"org.json.",
"org.lwjgl.",
"org.objectweb.asm.",
"org.w3c.",
"org.xml.",
"org.yaml.snakeyaml.",
"gnu.trove.",
"io.netty.",
"it.unimi.dsi.fastutil.",
"javassist.",
"jline.",
"joptsimple.",
"oracle.",
"oshi.",
"paulscode.",
};
private static void initIgnoredPackages() {
@ -202,7 +178,11 @@ public final class Config {
mojangNamespace = parseFeatureOption("authlibinjector.mojangNamespace");
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;
httpdPort = Integer.getInteger("authlibinjector.httpdPort", 0);
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.httpd;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_TEXT;
import java.io.IOException;
import java.util.Optional;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
/**
* Disables Mojang's anti-features.
*/
public class AntiFeaturesFilter implements URLFilter {
private static final String RESPONSE_PRIVILEGES = "{\"privileges\":{\"onlineChat\":{\"enabled\":true},\"multiplayerServer\":{\"enabled\":true},\"multiplayerRealms\":{\"enabled\":true},\"telemetry\":{\"enabled\":false}}}";
private static final String RESPONSE_PLAYER_ATTRIBUTES = "{\"privileges\":{\"multiplayerRealms\":{\"enabled\":true},\"multiplayerServer\":{\"enabled\":true},\"onlineChat\":{\"enabled\":true},\"telemetry\":{\"enabled\":false}},\"profanityFilterPreferences\":{\"profanityFilterOn\":false}}";
private static final String RESPONSE_PRIVACY_BLOCKLIST = "{\"blockedProfiles\":[]}";
@Override
public boolean canHandle(String domain) {
return domain.equals("api.minecraftservices.com") || domain.equals("sessionserver.mojang.com");
}
@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
if (domain.equals("api.minecraftservices.com") && path.equals("/privileges") && session.getMethod().equals("GET")) {
return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, RESPONSE_PRIVILEGES));
} else if (domain.equals("api.minecraftservices.com") && path.equals("/player/attributes") && session.getMethod().equals("GET")) {
return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, RESPONSE_PLAYER_ATTRIBUTES));
} else if (domain.equals("api.minecraftservices.com") && path.equals("/privacy/blocklist") && session.getMethod().equals("GET")) {
return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, RESPONSE_PRIVACY_BLOCKLIST));
} else if (domain.equals("sessionserver.mojang.com") && path.equals("/blockedservers") && session.getMethod().equals("GET")) {
return Optional.of(Response.newFixedLength(Status.NOT_FOUND, CONTENT_TYPE_TEXT, ""));
} else {
return Optional.empty();
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.httpd;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON;
import moe.yushi.authlibinjector.AuthlibInjector;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
import moe.yushi.authlibinjector.transform.PerformanceMetrics;
/**
* Authlib-injector's debug API
*/
public class DebugApiEndpoint {
public Response serve(IHTTPSession session) {
if (session.getUri().equals("/debug/metrics") && session.getMethod().equals("GET")) {
PerformanceMetrics metrics = AuthlibInjector.getClassTransformer().performanceMetrics;
JSONObject response = new JSONObject();
response.put("totalTime", metrics.getTotalTime());
response.put("matchTime", metrics.getMatchTime());
response.put("scanTime", metrics.getScanTime());
response.put("analysisTime", metrics.getAnalysisTime());
response.put("classesScanned", metrics.getClassesScanned());
response.put("classesSkipped", metrics.getClassesSkipped());
return Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, response.toJSONString());
} else {
return Response.newFixedLength(Status.NOT_FOUND, null, null);
}
}
}

View file

@ -37,6 +37,7 @@ public class DefaultURLRedirector implements URLRedirector {
domainMapping.put("authserver.mojang.com", "authserver");
domainMapping.put("sessionserver.mojang.com", "sessionserver");
domainMapping.put("skins.minecraft.net", "skins");
domainMapping.put("api.minecraftservices.com", "minecraftservices");
}
@Override

View file

@ -0,0 +1,84 @@
/*
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.httpd;
import static java.nio.charset.StandardCharsets.UTF_8;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON;
import java.io.IOException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Optional;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
/**
* Intercepts Minecraft's request to https://api.minecraftservices.com/player/certificates,
* and returns an empty response.
*/
public class ProfileKeyFilter implements URLFilter {
@Override
public boolean canHandle(String domain) {
return domain.equals("api.minecraftservices.com");
}
@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
if (domain.equals("api.minecraftservices.com") && path.equals("/player/certificates") && session.getMethod().equals("POST")) {
return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, makeDummyResponse().toJSONString()));
}
return Optional.empty();
}
private JSONObject makeDummyResponse() {
KeyPairGenerator generator;
try {
generator = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
generator.initialize(2048);
KeyPair keyPair = generator.generateKeyPair();
Base64.Encoder base64 = Base64.getMimeEncoder(76, "\n".getBytes(UTF_8));
String publicKeyPEM = "-----BEGIN RSA PUBLIC KEY-----\n" + base64.encodeToString(keyPair.getPublic().getEncoded()) + "\n-----END RSA PUBLIC KEY-----\n";
String privateKeyPEM = "-----BEGIN RSA PRIVATE KEY-----\n" + base64.encodeToString(keyPair.getPrivate().getEncoded()) + "\n-----END RSA PRIVATE KEY-----\n";
Instant now = Instant.now();
Instant expiresAt = now.plus(48, ChronoUnit.HOURS);
Instant refreshedAfter = now.plus(36, ChronoUnit.HOURS);
JSONObject response = new JSONObject();
JSONObject keyPairObj = new JSONObject();
keyPairObj.put("privateKey", privateKeyPEM);
keyPairObj.put("publicKey", publicKeyPEM);
response.put("keyPair", keyPairObj);
response.put("publicKeySignature", "AA==");
response.put("publicKeySignatureV2", "AA==");
response.put("expiresAt", DateTimeFormatter.ISO_INSTANT.format(expiresAt));
response.put("refreshedAfter", DateTimeFormatter.ISO_INSTANT.format(refreshedAfter));
return response;
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.httpd;
import static moe.yushi.authlibinjector.util.IOUtils.CONTENT_TYPE_JSON;
import java.io.IOException;
import java.security.PublicKey;
import java.util.Base64;
import java.util.Optional;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Response;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.Status;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONArray;
import moe.yushi.authlibinjector.internal.org.json.simple.JSONObject;
import moe.yushi.authlibinjector.transform.support.YggdrasilKeyTransformUnit;
public class PublickeysFilter implements URLFilter {
@Override
public boolean canHandle(String domain) {
return domain.equals("api.minecraftservices.com");
}
@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
if (domain.equals("api.minecraftservices.com") && path.equals("/publickeys") && session.getMethod().equals("GET")) {
return Optional.of(Response.newFixedLength(Status.OK, CONTENT_TYPE_JSON, makePublickeysResponse().toJSONString()));
}
return Optional.empty();
}
private JSONObject makePublickeysResponse() {
JSONObject response = new JSONObject();
JSONArray profilePropertyKeys = new JSONArray();
JSONArray playerCertificateKeys = new JSONArray();
for (PublicKey key : YggdrasilKeyTransformUnit.PUBLIC_KEYS) {
JSONObject entry = new JSONObject();
entry.put("publicKey", Base64.getEncoder().encodeToString(key.getEncoded()));
profilePropertyKeys.add(entry);
playerCertificateKeys.add(entry);
}
response.put("profilePropertyKeys", profilePropertyKeys);
response.put("playerCertificateKeys", playerCertificateKeys);
return response;
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2024 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -49,12 +49,15 @@ public class QueryUUIDsFilter implements URLFilter {
@Override
public boolean canHandle(String domain) {
return domain.equals("api.mojang.com");
return domain.equals("api.mojang.com") || domain.equals("api.minecraftservices.com");
}
@Override
public Optional<Response> handle(String domain, String path, IHTTPSession session) throws IOException {
if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && session.getMethod().equals("POST")) {
if (
(domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && session.getMethod().equals("POST")) ||
(domain.equals("api.minecraftservices.com") && path.equals("/minecraft/profile/lookup/bulk/byname") && session.getMethod().equals("POST"))
) {
Set<String> request = new LinkedHashSet<>();
asJsonArray(parseJson(asString(asBytes(session.getInputStream()))))
.forEach(element -> request.add(asJsonString(element)));

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -37,6 +37,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import moe.yushi.authlibinjector.Config;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IHTTPSession;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.IStatus;
import moe.yushi.authlibinjector.internal.fi.iki.elonen.NanoHTTPD;
@ -67,6 +68,10 @@ public class URLProcessor {
* @return the transformed URL, or empty if it doesn't need to be transformed
*/
public Optional<String> transformURL(String inputUrl) {
if (!inputUrl.startsWith("http")) {
// fast path
return Optional.empty();
}
Matcher matcher = URL_REGEX.matcher(inputUrl);
if (!matcher.find()) {
return Optional.empty();
@ -98,6 +103,7 @@ public class URLProcessor {
return redirector.redirect(domain, path);
}
private DebugApiEndpoint debugApi = new DebugApiEndpoint();
private volatile NanoHTTPD httpd;
private final Object httpdLock = new Object();
@ -117,9 +123,13 @@ public class URLProcessor {
}
private NanoHTTPD createHttpd() {
return new NanoHTTPD("127.0.0.1", 0) {
return new NanoHTTPD("127.0.0.1", Config.httpdPort) {
@Override
public Response serve(IHTTPSession session) {
if (session.getUri().startsWith("/debug/")) {
return debugApi.serve(session);
}
Matcher matcher = LOCAL_URL_REGEX.matcher(session.getUri());
if (matcher.find()) {
String protocol = matcher.group("protocol");
@ -178,7 +188,7 @@ public class URLProcessor {
conn.setDoOutput(clientIn != null);
requestHeaders.forEach(conn::setRequestProperty);
if (clientIn != null) {
if (clientIn != null && !method.equalsIgnoreCase("GET") && !method.equalsIgnoreCase("HEAD")) {
try (OutputStream upstreamOut = conn.getOutputStream()) {
transfer(clientIn, upstreamOut);
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -206,16 +206,18 @@ public class Response implements Closeable {
protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, long defaultSize) {
String contentLengthString = getHeader("content-length");
long size = defaultSize;
if (contentLengthString != null) {
if (contentLengthString == null) {
pw.print("Content-Length: " + defaultSize + "\r\n");
return defaultSize;
} else {
long size = defaultSize;
try {
size = Long.parseLong(contentLengthString);
} catch (NumberFormatException ex) {
log(ERROR, "content-length was not number " + contentLengthString);
}
return size;
}
pw.print("Content-Length: " + size + "\r\n");
return size;
}
private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException {

View file

@ -1,71 +0,0 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.NEW;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
class CallbackMetafactoryTransformer implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext context) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
MethodVisitor mv = super.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC,
CallbackSupport.METAFACTORY_NAME,
CallbackSupport.METAFACTORY_SIGNATURE,
null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, "java/lang/invoke/ConstantCallSite");
mv.visitInsn(DUP);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/ClassLoader", "getSystemClassLoader", "()Ljava/lang/ClassLoader;", false);
mv.visitVarInsn(ALOAD, 3);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/ClassLoader", "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", false);
mv.visitVarInsn(ALOAD, 1);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;", false);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/invoke/ConstantCallSite", "<init>", "(Ljava/lang/invoke/MethodHandle;)V", false);
mv.visitInsn(ARETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
context.markModified();
}
});
}
@Override
public String toString() {
return "Callback Metafactory Transformer";
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -16,17 +16,42 @@
*/
package moe.yushi.authlibinjector.transform;
import static org.objectweb.asm.Opcodes.AASTORE;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_STATIC;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ANEWARRAY;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.DLOAD;
import static org.objectweb.asm.Opcodes.DRETURN;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.FLOAD;
import static org.objectweb.asm.Opcodes.FRETURN;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import static org.objectweb.asm.Opcodes.LLOAD;
import static org.objectweb.asm.Opcodes.LRETURN;
import static org.objectweb.asm.Opcodes.NEW;
import static org.objectweb.asm.Opcodes.RETURN;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
public final class CallbackSupport {
final class CallbackSupport {
private CallbackSupport() {
}
static final String METAFACTORY_NAME = "__authlibinjector_metafactory";
static final String METAFACTORY_SIGNATURE = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;)Ljava/lang/invoke/CallSite;";
private static final String METAFACTORY_NAME = "__authlibinjector_metafactory";
private static final String METAFACTORY_SIGNATURE = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;)Ljava/lang/invoke/CallSite;";
private static Method findCallbackMethod(Class<?> owner, String methodName) {
for (Method method : owner.getDeclaredMethods()) {
@ -40,11 +65,123 @@ public final class CallbackSupport {
throw new IllegalArgumentException("No such method: " + methodName);
}
public static void invoke(TransformContext ctx, MethodVisitor mv, Class<?> owner, String methodName) {
ctx.requireMinimumClassVersion(50);
ctx.upgradeClassVersion(51);
static void callWithInvokeDynamic(MethodVisitor mv, Class<?> owner, String methodName, TransformContext ctx) {
String descriptor = Type.getMethodDescriptor(findCallbackMethod(owner, methodName));
mv.visitInvokeDynamicInsn(methodName, descriptor, ctx.acquireCallbackMetafactory(), owner.getName());
Handle callbackMetafactory = new Handle(
H_INVOKESTATIC,
ctx.getClassName().replace('.', '/'),
CallbackSupport.METAFACTORY_NAME,
CallbackSupport.METAFACTORY_SIGNATURE,
ctx.isInterface());
mv.visitInvokeDynamicInsn(methodName, descriptor, callbackMetafactory, owner.getName());
}
static void callWithIntermediateMethod(MethodVisitor mv0, Class<?> owner, String methodName, TransformContext ctx) {
Method callbackMethod = findCallbackMethod(owner, methodName);
String descriptor = Type.getMethodDescriptor(callbackMethod);
String intermediateMethod = "__authlibinjector_intermediate__" + owner.getName().replace('.', '_') + "__" + methodName;
mv0.visitMethodInsn(INVOKESTATIC, ctx.getClassName().replace('.', '/'), intermediateMethod, descriptor, ctx.isInterface());
ctx.addGeneratedMethod(intermediateMethod, cv -> {
int paramNum = callbackMethod.getParameterCount();
Class<?>[] paramTypes = callbackMethod.getParameterTypes();
Class<?> returnType = callbackMethod.getReturnType();
MethodVisitor mv = cv.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC, intermediateMethod, descriptor, null, null);
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "publicLookup", "()Ljava/lang/invoke/MethodHandles$Lookup;", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/ClassLoader", "getSystemClassLoader", "()Ljava/lang/ClassLoader;", false);
mv.visitLdcInsn(owner.getName());
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/ClassLoader", "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", false);
mv.visitLdcInsn(methodName);
pushType(mv, returnType);
mv.visitLdcInsn(paramNum);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Class");
for (int i = 0; i < paramNum; i++) {
mv.visitInsn(DUP);
mv.visitLdcInsn(i);
pushType(mv, paramTypes[i]);
mv.visitInsn(AASTORE);
}
mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodType", "methodType", "(Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;", false);
for (int i = 0; i < paramNum; i++) {
Class<?> type = paramTypes[i];
if (type == boolean.class || type == byte.class || type == char.class || type == short.class || type == int.class) {
mv.visitVarInsn(ILOAD, i);
} else if (type == long.class) {
mv.visitVarInsn(LLOAD, i);
} else if (type == float.class) {
mv.visitVarInsn(FLOAD, i);
} else if (type == double.class) {
mv.visitVarInsn(DLOAD, i);
} else {
mv.visitVarInsn(ALOAD, i);
}
}
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", descriptor, false);
if (returnType == void.class) {
mv.visitInsn(RETURN);
} else if (returnType == boolean.class || returnType == byte.class || returnType == char.class || returnType == short.class || returnType == int.class) {
mv.visitInsn(IRETURN);
} else if (returnType == long.class) {
mv.visitInsn(LRETURN);
} else if (returnType == float.class) {
mv.visitInsn(FRETURN);
} else if (returnType == double.class) {
mv.visitInsn(DRETURN);
} else {
mv.visitInsn(ARETURN);
}
mv.visitMaxs(-1, -1);
mv.visitEnd();
});
}
private static void pushType(MethodVisitor mv, Class<?> type) {
if (type.isPrimitive()) {
if (type == boolean.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;");
} else if (type == byte.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;");
} else if (type == char.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;");
} else if (type == short.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;");
} else if (type == int.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;");
} else if (type == float.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;");
} else if (type == long.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;");
} else if (type == double.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;");
} else if (type == void.class) {
mv.visitFieldInsn(GETSTATIC, "java/lang/Void", "TYPE", "Ljava/lang/Class;");
}
} else {
mv.visitLdcInsn(Type.getType(type));
}
}
static void insertMetafactory(ClassVisitor visitor) {
MethodVisitor mv = visitor.visitMethod(ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC,
CallbackSupport.METAFACTORY_NAME,
CallbackSupport.METAFACTORY_SIGNATURE,
null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, "java/lang/invoke/ConstantCallSite");
mv.visitInsn(DUP);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/ClassLoader", "getSystemClassLoader", "()Ljava/lang/ClassLoader;", false);
mv.visitVarInsn(ALOAD, 3);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/ClassLoader", "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", false);
mv.visitVarInsn(ALOAD, 1);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/invoke/MethodHandles$Lookup", "findStatic", "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;", false);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/invoke/ConstantCallSite", "<init>", "(Ljava/lang/invoke/MethodHandle;)V", false);
mv.visitInsn(ARETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -21,83 +21,91 @@ import static moe.yushi.authlibinjector.util.Logging.log;
import static moe.yushi.authlibinjector.util.Logging.Level.DEBUG;
import static moe.yushi.authlibinjector.util.Logging.Level.INFO;
import static moe.yushi.authlibinjector.util.Logging.Level.WARNING;
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
import static org.objectweb.asm.Opcodes.ACC_INTERFACE;
import static org.objectweb.asm.Opcodes.ASM9;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.Config;
public class ClassTransformer implements ClassFileTransformer {
public final List<TransformUnit> units = new CopyOnWriteArrayList<>();
public final List<ClassLoadingListener> listeners = new CopyOnWriteArrayList<>();
public final Set<String> ignores = Collections.newSetFromMap(new ConcurrentHashMap<>());
public final PerformanceMetrics performanceMetrics = new PerformanceMetrics();
private String[] ignores = new String[0];
private static class TransformContextImpl implements TransformContext {
private class TransformHandle {
private final String className;
private class TransformContextImpl implements TransformContext {
public boolean modifiedMark;
public int minVersionMark = -1;
public int upgradedVersionMark = -1;
public boolean callbackMetafactoryRequested = false;
public boolean modifiedMark;
public boolean callbackMetafactoryRequested = false;
public TransformContextImpl(String className) {
this.className = className;
}
@Override
public void markModified() {
modifiedMark = true;
}
@Override
public void markModified() {
modifiedMark = true;
}
@Override
public List<String> getStringConstants() {
return TransformHandle.this.getStringConstants();
}
@Override
public void requireMinimumClassVersion(int version) {
if (this.minVersionMark < version) {
this.minVersionMark = version;
@Override
public String getClassName() {
return className;
}
@Override
public boolean isInterface() {
return TransformHandle.this.isInterface();
}
@Override
public void invokeCallback(MethodVisitor mv, Class<?> owner, String methodName) {
boolean useInvokeDynamic = (getClassVersion() & 0xffff) >= 50;
if (useInvokeDynamic) {
addCallbackMetafactory = true;
CallbackSupport.callWithInvokeDynamic(mv, owner, methodName, this);
} else {
CallbackSupport.callWithIntermediateMethod(mv, owner, methodName, this);
}
}
@Override
public void addGeneratedMethod(String name, Consumer<ClassVisitor> generator) {
if (generatedMethods == null) {
generatedMethods = new LinkedHashMap<>();
}
generatedMethods.put(name, generator);
}
}
@Override
public void upgradeClassVersion(int version) {
if (this.upgradedVersionMark < version) {
this.upgradedVersionMark = version;
}
}
@Override
public Handle acquireCallbackMetafactory() {
this.callbackMetafactoryRequested = true;
return new Handle(
H_INVOKESTATIC,
className.replace('.', '/'),
CallbackSupport.METAFACTORY_NAME,
CallbackSupport.METAFACTORY_SIGNATURE,
false);
}
}
private static class TransformHandle {
private final String className;
private final ClassLoader classLoader;
private byte[] classBuffer;
private ClassReader cachedClassReader;
private List<String> cachedConstants;
private List<TransformUnit> appliedTransformers;
private int minVersion = -1;
private int upgradedVersion = -1;
private boolean addCallbackMetafactory = false;
private Map<String, Consumer<ClassVisitor>> generatedMethods;
public TransformHandle(ClassLoader classLoader, String className, byte[] classBuffer) {
this.className = className;
@ -105,51 +113,143 @@ public class ClassTransformer implements ClassFileTransformer {
this.classLoader = classLoader;
}
public void accept(TransformUnit unit) {
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
TransformContextImpl ctx = new TransformContextImpl(className);
private ClassReader getClassReader() {
if (cachedClassReader == null)
cachedClassReader = new ClassReader(classBuffer);
return cachedClassReader;
}
Optional<ClassVisitor> optionalVisitor = unit.transform(classLoader, className, writer, ctx);
if (optionalVisitor.isPresent()) {
ClassReader reader = new ClassReader(classBuffer);
reader.accept(optionalVisitor.get(), 0);
if (ctx.modifiedMark) {
log(INFO, "Transformed [" + className + "] with [" + unit + "]");
if (appliedTransformers == null) {
appliedTransformers = new ArrayList<>();
}
appliedTransformers.add(unit);
classBuffer = writer.toByteArray();
if (ctx.minVersionMark > this.minVersion) {
this.minVersion = ctx.minVersionMark;
}
if (ctx.upgradedVersionMark > this.upgradedVersion) {
this.upgradedVersion = ctx.upgradedVersionMark;
}
this.addCallbackMetafactory |= ctx.callbackMetafactoryRequested;
}
private boolean isInterface() {
return (getClassReader().getAccess() & ACC_INTERFACE) != 0;
}
private List<String> getStringConstants() {
if (cachedConstants == null)
cachedConstants = extractStringConstants(getClassReader());
return cachedConstants;
}
private int getClassVersion() {
ClassReader reader = getClassReader();
return reader.readInt(reader.getItem(1) - 7);
}
public void accept(TransformUnit... units) {
long t0 = System.nanoTime();
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
TransformContextImpl[] ctxs = new TransformContextImpl[units.length];
ClassVisitor chain = writer;
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())
continue;
ctxs[i] = ctx;
chain = visitor.get();
}
long t1 = System.nanoTime();
synchronized (performanceMetrics) {
performanceMetrics.scanTime += t1 - t0;
}
if (chain == writer)
return;
t0 = System.nanoTime();
getClassReader().accept(chain, 0);
t1 = System.nanoTime();
synchronized (performanceMetrics) {
performanceMetrics.analysisTime += t1 - t0;
}
boolean modified = false;
for (int i = 0; i < units.length; i++) {
TransformContextImpl ctx = ctxs[i];
if (ctx == null || !ctx.modifiedMark)
continue;
log(INFO, "Transformed [" + className + "] with [" + units[i] + "]");
if (appliedTransformers == null)
appliedTransformers = new ArrayList<>();
appliedTransformers.add(units[i]);
this.addCallbackMetafactory |= ctx.callbackMetafactoryRequested;
modified = true;
}
if (modified) {
updateClassBuffer(writer.toByteArray());
}
}
private void injectCallbackMetafactory() {
log(DEBUG, "Adding callback metafactory");
int classVersion = getClassVersion();
int majorVersion = classVersion & 0xffff;
int newVersion;
if (majorVersion < 51) {
newVersion = 51;
log(DEBUG, "Upgrading class version from " + classVersion + " to " + newVersion);
} else {
newVersion = classVersion;
}
ClassReader reader = getClassReader();
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(newVersion, access, name, signature, superName, interfaces);
CallbackSupport.insertMetafactory(this);
}
};
reader.accept(visitor, 0);
updateClassBuffer(writer.toByteArray());
}
private void injectGeneratedMethods() {
ClassReader reader = getClassReader();
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
for (Entry<String, Consumer<ClassVisitor>> el : generatedMethods.entrySet()) {
log(DEBUG, "Adding generated method [" + el.getKey() + "]");
el.getValue().accept(this);
}
}
};
reader.accept(visitor, 0);
updateClassBuffer(writer.toByteArray());
}
private void updateClassBuffer(byte[] buf) {
classBuffer = buf;
cachedClassReader = null;
cachedConstants = null;
}
public Optional<byte[]> finish() {
if (appliedTransformers == null || appliedTransformers.isEmpty()) {
return Optional.empty();
} else {
if (addCallbackMetafactory) {
accept(new CallbackMetafactoryTransformer());
}
if (minVersion == -1 && upgradedVersion == -1) {
return Optional.of(classBuffer);
} else {
try {
accept(new ClassVersionTransformUnit(minVersion, upgradedVersion));
return Optional.of(classBuffer);
} catch (ClassVersionException e) {
log(WARNING, "Skipping [" + className + "], " + e.getMessage());
return Optional.empty();
}
}
}
if (addCallbackMetafactory) {
injectCallbackMetafactory();
}
if (generatedMethods != null) {
injectGeneratedMethods();
}
return Optional.of(classBuffer);
}
public List<TransformUnit> getAppliedTransformers() {
@ -165,22 +265,43 @@ public class ClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String internalClassName, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (internalClassName != null && classfileBuffer != null) {
try {
long t0 = System.nanoTime();
String className = internalClassName.replace('/', '.');
for (String prefix : ignores) {
if (className.startsWith(prefix)) {
for (String ignore : ignores) {
if (className.startsWith(ignore)) {
listeners.forEach(it -> it.onClassLoading(loader, className, classfileBuffer, Collections.emptyList()));
long t1 = System.nanoTime();
synchronized (performanceMetrics) {
performanceMetrics.classesSkipped++;
performanceMetrics.totalTime += t1 - t0;
performanceMetrics.matchTime += t1 - t0;
}
return null;
}
}
long t1 = System.nanoTime();
TransformHandle handle = new TransformHandle(loader, className, classfileBuffer);
units.forEach(handle::accept);
listeners.forEach(it -> it.onClassLoading(loader, className, handle.getFinalResult(), handle.getAppliedTransformers()));
TransformUnit[] unitsArray = units.toArray(new TransformUnit[0]);
handle.accept(unitsArray);
Optional<byte[]> transformResult = handle.finish();
if (Config.printUntransformedClass && !transformResult.isPresent()) {
log(DEBUG, "No transformation is applied to [" + className + "]");
}
listeners.forEach(it -> it.onClassLoading(loader, className, handle.getFinalResult(), handle.getAppliedTransformers()));
long t2 = System.nanoTime();
synchronized (performanceMetrics) {
performanceMetrics.classesScanned++;
performanceMetrics.totalTime += t2 - t0;
performanceMetrics.matchTime += t1 - t0;
}
return transformResult.orElse(null);
} catch (Throwable e) {
log(WARNING, "Failed to transform [" + internalClassName + "]", e);
@ -188,4 +309,25 @@ public class ClassTransformer implements ClassFileTransformer {
}
return null;
}
private static List<String> extractStringConstants(ClassReader reader) {
List<String> constants = new ArrayList<>();
int constantPoolSize = reader.getItemCount();
char[] buf = new char[reader.getMaxStringLength()];
for (int idx = 1; idx < constantPoolSize; idx++) {
int offset = reader.getItem(idx);
if (offset == 0)
continue;
int type = reader.readByte(offset - 1);
if (type == 8) { // CONSTANT_String_info
String constant = (String) reader.readConst(idx, buf);
constants.add(constant);
}
}
return constants;
}
public void setIgnores(Collection<String> newIgnores) {
ignores = newIgnores.toArray(ignores);
}
}

View file

@ -1,60 +0,0 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform;
import static moe.yushi.authlibinjector.util.Logging.log;
import static moe.yushi.authlibinjector.util.Logging.Level.DEBUG;
import static org.objectweb.asm.Opcodes.ASM9;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
class ClassVersionTransformUnit implements TransformUnit {
private final int minVersion;
private final int upgradedVersion;
public ClassVersionTransformUnit(int minVersion, int upgradedVersion) {
this.minVersion = minVersion;
this.upgradedVersion = upgradedVersion;
}
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext context) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
int major = version & 0xffff;
if (minVersion != -1 && major < minVersion) {
throw new ClassVersionException("class version (" + major + ") is lower than required(" + minVersion + ")");
}
if (upgradedVersion != -1 && major < upgradedVersion) {
log(DEBUG,"Upgrading class version from " + major + " to " + upgradedVersion);
version = upgradedVersion;
context.markModified();
}
super.visit(version, access, name, signature, superName, interfaces);
}
});
}
@Override
public String toString() {
return "Class File Version Transformer";
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -19,12 +19,24 @@ package moe.yushi.authlibinjector.transform;
import static org.objectweb.asm.Opcodes.ASM9;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
public abstract class LdcTransformUnit implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) {
boolean matched = false;
for (String constant : ctx.getStringConstants()) {
Optional<String> transformed = transformLdc(constant);
if (transformed.isPresent() && !transformed.get().equals(constant)) {
matched = true;
break;
}
}
if (!matched)
return Optional.empty();
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
@ -45,6 +57,21 @@ public abstract class LdcTransformUnit implements TransformUnit {
super.visitLdcInsn(cst);
}
}
@Override
public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
for (int i = 0; i < bootstrapMethodArguments.length; i++) {
if (bootstrapMethodArguments[i] instanceof String) {
String constant = (String) bootstrapMethodArguments[i];
Optional<String> transformed = transformLdc(constant);
if (transformed.isPresent() && !transformed.get().equals(constant)) {
ctx.markModified();
bootstrapMethodArguments[i] = transformed.get();
}
}
}
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
}
};
}
});

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -16,8 +16,19 @@
*/
package moe.yushi.authlibinjector.transform;
class ClassVersionException extends RuntimeException {
public ClassVersionException(String message) {
super(message);
}
public class PerformanceMetrics {
volatile long totalTime;
volatile long matchTime;
volatile long scanTime;
volatile long analysisTime;
volatile long classesScanned;
volatile long classesSkipped;
public synchronized long getTotalTime() { return totalTime; }
public synchronized long getMatchTime() { return matchTime; }
public synchronized long getScanTime() { return scanTime; }
public synchronized long getAnalysisTime() { return analysisTime; }
public synchronized long getClassesScanned() { return classesScanned; }
public synchronized long getClassesSkipped() { return classesSkipped; }
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -16,15 +16,22 @@
*/
package moe.yushi.authlibinjector.transform;
import org.objectweb.asm.Handle;
import java.util.List;
import java.util.function.Consumer;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
public interface TransformContext {
String getClassName();
boolean isInterface();
void markModified();
void requireMinimumClassVersion(int version);
List<String> getStringConstants();
void upgradeClassVersion(int version);
void invokeCallback(MethodVisitor mv, Class<?> owner, String methodName);
Handle acquireCallbackMetafactory();
void addGeneratedMethod(String name, Consumer<ClassVisitor> generator);
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform.support;
import static moe.yushi.authlibinjector.util.Logging.log;
import static moe.yushi.authlibinjector.util.Logging.Level.INFO;
public class AccountTypeTransformer {
public String[] transform(String[] args) {
boolean userTypeMatched = false;
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if ("--userType".equals(arg)) {
userTypeMatched = true;
} else if (userTypeMatched && "mojang".equals(arg)) {
args[i] = "msa";
log(INFO, "Setting accountType to msa");
break;
}
}
return args;
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2023 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -37,7 +37,6 @@ import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import moe.yushi.authlibinjector.transform.CallbackMethod;
import moe.yushi.authlibinjector.transform.CallbackSupport;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
@ -129,7 +128,18 @@ public class AuthlibLogInterceptor implements TransformUnit {
Array.set(appenderRefs, 0, appenderRef);
Object loggerConfig;
{
try {
Object builder = classLoggerConfig.getDeclaredMethod("newBuilder").invoke(null);
Class<?> classBuilder = cl.loadClass("org.apache.logging.log4j.core.config.LoggerConfig$Builder");
classBuilder.getMethod("withConfig", classConfiguration).invoke(builder, configuration);
classBuilder.getMethod("withAdditivity", boolean.class).invoke(builder, false);
classBuilder.getMethod("withLevel", classLevel).invoke(builder, classLevel.getDeclaredField("ALL").get(null));
classBuilder.getMethod("withLoggerName", String.class).invoke(builder, loggerName);
classBuilder.getMethod("withIncludeLocation", String.class).invoke(builder, authlibPackageName);
classBuilder.getMethod("withRefs", appenderRefs.getClass()).invoke(builder, appenderRefs);
loggerConfig = classBuilder.getMethod("build").invoke(builder);
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
Map<String, Object> values = new HashMap<>();
values.put("additivity", false);
values.put("level", classLevel.getDeclaredField("ALL").get(null));
@ -232,7 +242,7 @@ public class AuthlibLogInterceptor implements TransformUnit {
super.visitCode();
super.visitLdcInsn(Type.getType("L" + className.replace('.', '/') + ";"));
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getClassLoader", "()Ljava/lang/ClassLoader;", false);
CallbackSupport.invoke(ctx, mv, AuthlibLogInterceptor.class, "onClassLoading");
ctx.invokeCallback(mv, AuthlibLogInterceptor.class, "onClassLoading");
ctx.markModified();
}
};

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform.support;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.ISTORE;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
/**
* Hacks BungeeCord to allow special characters to occur in the username.
*
* Since <https://github.com/SpigotMC/BungeeCord/commit/3008d7ef2f50de7e3d38e76717df72dac7fe0da3>,
* BungeeCord allows only certain characters to occur in the username when online-mode is on.
*/
public class BungeeCordAllowedCharactersTransformer implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext context) {
if ("net.md_5.bungee.util.AllowedCharacters".equals(className)) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if ("isValidName".equals(name) && "(Ljava/lang/String;Z)Z".equals(descriptor)) {
return new MethodVisitor(ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) {
@Override
public void visitCode() {
super.visitCode();
super.visitLdcInsn(0);
super.visitVarInsn(ISTORE, 1);
context.markModified();
}
};
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
});
}
return Optional.empty();
}
@Override
public String toString() {
return "BungeeCord Allowed Characters Transformer";
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform.support;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.ICONST_1;
import static org.objectweb.asm.Opcodes.IRETURN;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
/**
* Hacks BungeeCord to bypass profile key signature validation.
* See https://github.com/SpigotMC/BungeeCord/commit/78ca16dfe3bf9a21d5c054a1884d4f5f198a62bc .
*/
public class BungeeCordProfileKeyTransformUnit implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) {
if ("net.md_5.bungee.EncryptionUtil".equals(className)) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if ("check".equals(name) && "(Lnet/md_5/bungee/protocol/PlayerPublicKey;Ljava/util/UUID;)Z".equals(descriptor)) {
ctx.markModified();
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
mv.visitCode();
mv.visitInsn(ICONST_1);
mv.visitInsn(IRETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
return null;
} else {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
});
} else {
return Optional.empty();
}
}
@Override
public String toString() {
return "BungeeCord Profile Key Transformer";
}
}

View file

@ -0,0 +1,80 @@
/*
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform.support;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM9;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.CallbackMethod;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
/**
* See <https://github.com/yushijinhun/authlib-injector/issues/126>
*/
public class ConcatenateURLTransformUnit implements TransformUnit {
@CallbackMethod
public static URL concatenateURL(URL url, String query) {
try {
if (url.getQuery() != null && url.getQuery().length() > 0) {
return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + "&" + query);
} else {
return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + "?" + query);
}
} catch (MalformedURLException ex) {
throw new IllegalArgumentException("Could not concatenate given URL with GET arguments!", ex);
}
}
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) {
if ("com.mojang.authlib.HttpAuthenticationService".equals(className)) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if ("concatenateURL".equals(name) && "(Ljava/net/URL;Ljava/lang/String;)Ljava/net/URL;".equals(descriptor)) {
ctx.markModified();
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
ctx.invokeCallback(mv, ConcatenateURLTransformUnit.class, "concatenateURL");
mv.visitInsn(ARETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
return null;
} else {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
});
} else {
return Optional.empty();
}
}
@Override
public String toString() {
return "ConcatenateURL Workaround";
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -33,7 +33,6 @@ import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.AuthlibInjector;
import moe.yushi.authlibinjector.transform.CallbackMethod;
import moe.yushi.authlibinjector.transform.CallbackSupport;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
import moe.yushi.authlibinjector.util.WeakIdentityHashMap;
@ -120,7 +119,7 @@ public class MC52974_1710Workaround {
if (opcode == ARETURN) {
ctx.markModified();
super.visitInsn(DUP);
CallbackSupport.invoke(ctx, mv, MC52974_1710Workaround.class, "markGameProfile");
ctx.invokeCallback(mv, MC52974_1710Workaround.class, "markGameProfile");
}
super.visitInsn(opcode);
}
@ -163,7 +162,7 @@ public class MC52974_1710Workaround {
super.visitMethodInsn(INVOKESTATIC, "net/minecraft/server/MinecraftServer", "func_71276_C", "()Lnet/minecraft/server/MinecraftServer;", false);
}
super.visitLdcInsn(isNotchName ? 1 : 0);
CallbackSupport.invoke(ctx, mv, MC52974_1710Workaround.class, "accessGameProfile");
ctx.invokeCallback(mv, MC52974_1710Workaround.class, "accessGameProfile");
super.visitTypeInsn(CHECKCAST, "com/mojang/authlib/GameProfile");
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -31,7 +31,6 @@ import java.util.stream.Stream;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.CallbackMethod;
import moe.yushi.authlibinjector.transform.CallbackSupport;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
@ -51,7 +50,7 @@ public class MainArgumentsTransformer implements TransformUnit {
ctx.markModified();
super.visitVarInsn(ALOAD, 0);
CallbackSupport.invoke(ctx, mv, MainArgumentsTransformer.class, "processMainArguments");
ctx.invokeCallback(mv, MainArgumentsTransformer.class, "processMainArguments");
super.visitVarInsn(ASTORE, 0);
}
};

View file

@ -0,0 +1,62 @@
/*
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform.support;
import static org.objectweb.asm.Opcodes.*;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
/**
* Disables PaperMC's username check.
* See <https://github.com/PaperMC/Paper/blob/master/patches/server/0823-Validate-usernames.patch>.
*/
public class PaperUsernameCheckTransformer implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext context) {
if (!context.getStringConstants().contains("Invalid characters in username")) {
return Optional.empty();
}
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return new MethodVisitor(ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) {
@Override
public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
if (opcode == GETFIELD && "iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation".equals(name)) {
context.markModified();
visitInsn(POP);
visitInsn(ICONST_1);
} else {
super.visitFieldInsn(opcode, owner, name, descriptor);
}
}
};
}
});
}
@Override
public String toString() {
return "Paper Username Check Transformer";
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2023 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -27,7 +27,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.CallbackMethod;
import moe.yushi.authlibinjector.transform.CallbackSupport;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
@ -50,6 +49,12 @@ public class SkinWhitelistTransformUnit implements TransformUnit {
".mojang.com"
};
private static final String[] DEFAULT_BLACKLISTED_DOMAINS = {
"education.minecraft.net",
"bugs.mojang.com",
"feedback.minecraft.net"
};
private static final List<String> WHITELISTED_DOMAINS = new CopyOnWriteArrayList<>();
public static List<String> getWhitelistedDomains() {
@ -58,6 +63,7 @@ public class SkinWhitelistTransformUnit implements TransformUnit {
@CallbackMethod
public static boolean isWhitelistedDomain(String url) {
System.out.println(url);
String domain;
try {
domain = new URI(url).getHost();
@ -65,6 +71,12 @@ public class SkinWhitelistTransformUnit implements TransformUnit {
throw new IllegalArgumentException("Invalid URL '" + url + "'");
}
for (String pattern : DEFAULT_BLACKLISTED_DOMAINS) {
if (domainMatches(pattern, domain)) {
return false;
}
}
for (String pattern : DEFAULT_WHITELISTED_DOMAINS) {
if (domainMatches(pattern, domain)) {
return true;
@ -80,17 +92,18 @@ public class SkinWhitelistTransformUnit implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) {
if ("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService".equals(className)) {
if ("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService".equals(className) || "com.mojang.authlib.yggdrasil.TextureUrlChecker".equals(className)) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if ("isWhitelistedDomain".equals(name) && "(Ljava/lang/String;)Z".equals(desc)) {
if (("isWhitelistedDomain".equals(name) || "isAllowedTextureDomain".equals(name)) &&
"(Ljava/lang/String;)Z".equals(desc)) {
ctx.markModified();
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
CallbackSupport.invoke(ctx, mv, SkinWhitelistTransformUnit.class, "isWhitelistedDomain");
ctx.invokeCallback(mv, SkinWhitelistTransformUnit.class, "isWhitelistedDomain");
mv.visitInsn(IRETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();

View file

@ -0,0 +1,102 @@
/*
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform.support;
import static org.objectweb.asm.Opcodes.*;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
/**
* Starting from 22w06a, Minecraft allows only certain ASCII characters (33 ~ 126)
* in the username. This transformer removes the restriction.
*/
public class UsernameCharacterCheckTransformer implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext context) {
if (!context.getStringConstants().contains("Invalid characters in username")) {
return Optional.empty();
}
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return new MethodVisitor(ASM9, super.visitMethod(access, name, descriptor, signature, exceptions)) {
// States:
// 0 - initial state
// 1 - ldc_w "Invalid characters in username"
// 2 - iconst_0
// 3 - anewarray java/lang/Object
// 4 - invokestatic org/apache/commons/lang3/Validate.validState:(ZLjava/lang/String;[Ljava/lang/Object;)V
int state = 0;
@Override
public void visitLdcInsn(Object value) {
if (state == 0 && "Invalid characters in username".equals(value)) {
state++;
}
super.visitLdcInsn(value);
}
@Override
public void visitInsn(int opcode) {
if (state == 1 && opcode == ICONST_0) {
state++;
}
super.visitInsn(opcode);
}
@Override
public void visitTypeInsn(int opcode, String type) {
if (state == 2 && opcode == ANEWARRAY && "java/lang/Object".equals(type)) {
state++;
}
super.visitTypeInsn(opcode, type);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (state == 3 &&
opcode == INVOKESTATIC &&
"org/apache/commons/lang3/Validate".equals(owner) &&
"validState".equals(name) &&
"(ZLjava/lang/String;[Ljava/lang/Object;)V".equals(descriptor)) {
context.markModified();
state++;
super.visitInsn(POP);
super.visitInsn(POP);
super.visitInsn(POP);
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
};
}
});
}
@Override
public String toString() {
return "Username Character Check Transformer";
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package moe.yushi.authlibinjector.transform.support;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import java.util.Optional;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
/**
* Hacks Velocity to bypass profile key signature validation.
* See https://github.com/PaperMC/Velocity/commit/1a3fba4250553702d9dcd05731d04347bfc24c9f .
*/
public class VelocityProfileKeyTransformUnit implements TransformUnit {
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) {
if ("com.velocitypowered.proxy.crypto.IdentifiedKeyImpl".equals(className)) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if ("validateData".equals(name) && "(Ljava/util/UUID;)Ljava/lang/Boolean;".equals(descriptor)) {
ctx.markModified();
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;");
mv.visitInsn(ARETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
return null;
} else {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}
});
} else {
return Optional.empty();
}
}
@Override
public String toString() {
return "Velocity Profile Key Transformer";
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2023 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -16,62 +16,206 @@
*/
package moe.yushi.authlibinjector.transform.support;
import static java.lang.invoke.MethodHandles.publicLookup;
import static java.lang.invoke.MethodType.methodType;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
import static moe.yushi.authlibinjector.util.Logging.Level.DEBUG;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.H_INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Opcodes.IRETURN;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import moe.yushi.authlibinjector.transform.CallbackMethod;
import moe.yushi.authlibinjector.transform.CallbackSupport;
import moe.yushi.authlibinjector.transform.TransformContext;
import moe.yushi.authlibinjector.transform.TransformUnit;
import moe.yushi.authlibinjector.util.KeyUtils;
import moe.yushi.authlibinjector.util.Logging;
import moe.yushi.authlibinjector.util.Logging.Level;
public class YggdrasilKeyTransformUnit implements TransformUnit {
public static final List<PublicKey> PUBLIC_KEYS = new CopyOnWriteArrayList<>();
@CallbackMethod
public static boolean verifyPropertySignature(Object property, PublicKey mojangKey, MethodHandle verifyAction) throws Throwable {
if ((boolean) verifyAction.invoke(property, mojangKey)) {
return true;
static {
PUBLIC_KEYS.add(loadMojangPublicKey());
}
private static PublicKey loadMojangPublicKey() {
try (InputStream in = YggdrasilKeyTransformUnit.class.getResourceAsStream("/mojang_publickey.der")) {
return KeyUtils.parseX509PublicKey(asBytes(in));
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException("Failed to load Mojang public key", e);
}
}
@CallbackMethod
public static boolean verifyPropertySignature(Object propertyObj) {
String base64Signature;
String propertyValue;
try {
MethodHandle valueHandle;
try {
valueHandle = publicLookup().findVirtual(propertyObj.getClass(), "getValue", methodType(String.class));
} catch (NoSuchMethodException ignored) {
valueHandle = publicLookup().findVirtual(propertyObj.getClass(), "value", methodType(String.class));
}
MethodHandle signatureHandle;
try {
signatureHandle = publicLookup().findVirtual(propertyObj.getClass(), "getSignature", methodType(String.class));
} catch(NoSuchMethodException ignored) {
signatureHandle = publicLookup().findVirtual(propertyObj.getClass(), "signature", methodType(String.class));
}
base64Signature = (String) signatureHandle.invokeWithArguments(propertyObj);
propertyValue = (String) valueHandle.invokeWithArguments(propertyObj);
} catch (Throwable e) {
Logging.log(Level.ERROR, "Failed to get property attributes", e);
return false;
}
byte[] sig = Base64.getDecoder().decode(base64Signature);
byte[] data = propertyValue.getBytes();
for (PublicKey customKey : PUBLIC_KEYS) {
if ((boolean) verifyAction.invoke(property, customKey)) {
return true;
try {
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initVerify(customKey);
signature.update(data);
if (signature.verify(sig))
return true;
} catch (GeneralSecurityException e) {
Logging.log(DEBUG, "Failed to verify signature with key " + customKey, e);
}
}
Logging.log(Level.WARNING, "Failed to verify property signature");
return false;
}
@CallbackMethod
public static Signature createDummySignature() {
Signature sig = new Signature("authlib-injector-dummy-verify") {
@Override
protected boolean engineVerify(byte[] sigBytes) {
return true;
}
@Override
protected void engineUpdate(byte[] b, int off, int len) {
}
@Override
protected void engineUpdate(byte b) {
}
@Override
protected byte[] engineSign() {
throw new UnsupportedOperationException();
}
@Override
@Deprecated
protected void engineSetParameter(String param, Object value) {
}
@Override
protected void engineInitVerify(PublicKey publicKey) {
}
@Override
protected void engineInitSign(PrivateKey privateKey) {
throw new UnsupportedOperationException();
}
@Override
@Deprecated
protected Object engineGetParameter(String param) {
return null;
}
};
try {
sig.initVerify((PublicKey) null);
} catch (InvalidKeyException e) {
throw new RuntimeException(e);
}
return sig;
}
@Override
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) {
if ("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService".equals(className)) {
if ("com.mojang.authlib.properties.Property".equals(className)) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new MethodVisitor(ASM9, super.visitMethod(access, name, desc, signature, exceptions)) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if (opcode == INVOKEVIRTUAL
&& "com/mojang/authlib/properties/Property".equals(owner)
&& "isSignatureValid".equals(name)
&& "(Ljava/security/PublicKey;)Z".equals(descriptor)) {
ctx.markModified();
super.visitLdcInsn(new Handle(H_INVOKEVIRTUAL, owner, name, descriptor, isInterface));
CallbackSupport.invoke(ctx, this, YggdrasilKeyTransformUnit.class, "verifyPropertySignature");
} else {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
};
}
if ("isSignatureValid".equals(name) && "(Ljava/security/PublicKey;)Z".equals(desc)) {
ctx.markModified();
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "verifyPropertySignature");
mv.visitInsn(IRETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
return null;
} else {
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
});
} else if ("com.mojang.authlib.yggdrasil.YggdrasilServicesKeyInfo".equals(className)) {
return Optional.of(new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if ("validateProperty".equals(name) && "(Lcom/mojang/authlib/properties/Property;)Z".equals(desc)) {
ctx.markModified();
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
mv.visitCode();
mv.visitVarInsn(ALOAD, 1);
ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "verifyPropertySignature");
mv.visitInsn(IRETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
return null;
} else if ("signature".equals(name) && "()Ljava/security/Signature;".equals(desc)) {
ctx.markModified();
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
mv.visitCode();
ctx.invokeCallback(mv, YggdrasilKeyTransformUnit.class, "createDummySignature");
mv.visitInsn(ARETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
return null;
} else {
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
});
} else {
return Optional.empty();

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -29,11 +29,14 @@ import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.regex.Pattern;
import moe.yushi.authlibinjector.Config;
public final class Logging {
private Logging() {}
private static final Pattern CONTROL_CHARACTERS_FILTER = Pattern.compile("[\\p{Cc}&&[^\r\n\t]]");
private static final PrintStream out = System.err;
private static final FileChannel logfile = openLogFile();
@ -83,13 +86,12 @@ public final class Logging {
log += sw.toString();
}
// remove control characters to prevent messing up the console
log = log.replaceAll("[\\p{Cc}&&[^\r\n\t]]", "");
log = CONTROL_CHARACTERS_FILTER.matcher(log).replaceAll("");
out.println(log);
if (logfile != null) {
try {
logfile.write(Charset.defaultCharset().encode(log + System.lineSeparator()));
logfile.force(true);
} catch (IOException ex) {
out.println("[authlib-injector] [ERROR] Error writing to log file: " + ex);
}

Binary file not shown.

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -18,15 +18,14 @@ package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Test;
import org.junit.jupiter.api.Test;
@SuppressWarnings("resource")
public class ChunkedInputStreamTest {
@ -76,99 +75,99 @@ public class ChunkedInputStreamTest {
assertEquals(underlying.read(), -1);
}
@Test(expected = EOFException.class)
@Test
public void testReadEOF1() throws IOException {
byte[] data = ("a").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(EOFException.class, () -> asBytes(in));
}
@Test(expected = EOFException.class)
@Test
public void testReadEOF2() throws IOException {
byte[] data = ("a\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(EOFException.class, () -> asBytes(in));
}
@Test(expected = EOFException.class)
@Test
public void testReadEOF3() throws IOException {
byte[] data = ("a\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(EOFException.class, () -> asBytes(in));
}
@Test(expected = EOFException.class)
@Test
public void testReadEOF4() throws IOException {
byte[] data = ("a\r\nabc").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(EOFException.class, () -> asBytes(in));
}
@Test(expected = EOFException.class)
@Test
public void testReadEOF5() throws IOException {
byte[] data = ("a\r\n123456789a\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(EOFException.class, () -> asBytes(in));
}
@Test(expected = EOFException.class)
@Test
public void testReadEOF6() throws IOException {
byte[] data = ("a\r\n123456789a\r\n").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(EOFException.class, () -> asBytes(in));
}
@Test(expected = EOFException.class)
@Test
public void testReadEOF7() throws IOException {
byte[] data = ("a\r\n123456789a\r\n0\r\n\r").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(EOFException.class, () -> asBytes(in));
}
@Test(expected = IOException.class)
@Test
public void testBadIn1() throws IOException {
byte[] data = ("-1").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(IOException.class, () -> asBytes(in));
}
@Test(expected = IOException.class)
@Test
public void testBadIn2() throws IOException {
byte[] data = ("a\ra").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(IOException.class, () -> asBytes(in));
}
@Test(expected = IOException.class)
@Test
public void testBadIn3() throws IOException {
byte[] data = ("a\r\n123456789aa").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(IOException.class, () -> asBytes(in));
}
@Test(expected = IOException.class)
@Test
public void testBadIn4() throws IOException {
byte[] data = ("a\r\n123456789a\ra").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(IOException.class, () -> asBytes(in));
}
@Test(expected = IOException.class)
@Test
public void testBadIn5() throws IOException {
byte[] data = ("a\r\n123456789a\r\n0\r\n\r-").getBytes(US_ASCII);
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
InputStream in = new ChunkedInputStream(underlying);
asBytes(in);
assertThrows(IOException.class, () -> asBytes(in));
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -17,16 +17,15 @@
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import org.junit.Test;
import org.junit.jupiter.api.Test;
@SuppressWarnings("resource")
public class FixedLengthInputStreamTest {
@ -58,10 +57,10 @@ public class FixedLengthInputStreamTest {
assertEquals(underlying.read(), 0x11);
}
@Test(expected = EOFException.class)
@Test
public void testReadEOF() throws IOException {
byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
InputStream in = new FixedLengthInputStream(new ByteArrayInputStream(data), 6);
asBytes(in);
assertThrows(EOFException.class, () -> asBytes(in));
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -18,9 +18,9 @@ package moe.yushi.authlibinjector.test;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import moe.yushi.authlibinjector.APIMetadata;
import moe.yushi.authlibinjector.httpd.DefaultURLRedirector;

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -17,8 +17,9 @@
package moe.yushi.authlibinjector.test;
import static moe.yushi.authlibinjector.util.KeyUtils.decodePEMPublicKey;
import static org.junit.Assert.assertArrayEquals;
import org.junit.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
public class KeyUtilsTest {
@ -34,14 +35,16 @@ public class KeyUtilsTest {
decodePEMPublicKey("-----BEGIN PUBLIC KEY-----\nf\n39/fw==\n-----END PUBLIC KEY-----\n"));
}
@Test(expected = IllegalArgumentException.class)
@Test
public void testDecodePublicKey3() {
decodePEMPublicKey("-----BEGIN PUBLIC KEY----- f39/fw== -----END PUBLIC KEY-----");
assertThrows(IllegalArgumentException.class,
() -> decodePEMPublicKey("-----BEGIN PUBLIC KEY----- f39/fw== -----END PUBLIC KEY-----"));
}
@Test(expected = IllegalArgumentException.class)
@Test
public void testDecodePublicKey4() {
decodePEMPublicKey("-----BEGIN PUBLIC KEY-----f39/fw==-----END NOT A PUBLIC KEY-----");
assertThrows(IllegalArgumentException.class,
() -> decodePEMPublicKey("-----BEGIN PUBLIC KEY-----f39/fw==-----END NOT A PUBLIC KEY-----"));
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -17,9 +17,9 @@
package moe.yushi.authlibinjector.test;
import static moe.yushi.authlibinjector.transform.support.SkinWhitelistTransformUnit.domainMatches;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public class SkinWhitelistTest {

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Haowei Wen <yushijinhun@gmail.com> and contributors
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -17,11 +17,9 @@
package moe.yushi.authlibinjector.test;
import static moe.yushi.authlibinjector.transform.support.MainArgumentsTransformer.inferVersionSeries;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.Optional;
import org.junit.Test;
import org.junit.jupiter.api.Test;
public class VersionSeriesDetectTest {