forked from MirrorHub/authlib-injector
Compare commits
1 commit
develop
...
feature/do
Author | SHA1 | Date | |
---|---|---|---|
999ff99751 |
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
|
@ -11,22 +11,20 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
- name: Setup JDK 8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
java-version: 8
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew
|
||||
run: gradle
|
||||
|
||||
- name: Test
|
||||
run: ./gradlew test
|
||||
run: gradle test
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
path: build/libs/*
|
||||
|
|
14
.github/workflows/create_release.yml
vendored
14
.github/workflows/create_release.yml
vendored
|
@ -66,16 +66,14 @@ jobs:
|
|||
Build number: ${{ steps.parse_pr.outputs.build_number }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ steps.parse_pr.outputs.commit }}
|
||||
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
- name: Setup JDK 8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
java-version: 8
|
||||
|
||||
- id: build
|
||||
name: Build
|
||||
|
@ -83,8 +81,8 @@ jobs:
|
|||
run: |
|
||||
export AI_BUILD_NUMBER=${{ steps.parse_pr.outputs.build_number }}
|
||||
export AI_VERSION_NUMBER=${{ steps.parse_pr.outputs.version_number }}
|
||||
./gradlew
|
||||
./gradlew test
|
||||
gradle
|
||||
gradle test
|
||||
asset_path=$(echo build/libs/*.jar)
|
||||
echo "Build output is at $asset_path"
|
||||
echo "::set-output name=asset_path::$asset_path"
|
||||
|
|
7
.github/workflows/deploy_release.yml
vendored
7
.github/workflows/deploy_release.yml
vendored
|
@ -38,11 +38,10 @@ 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 "github-actions[bot]"
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "authlib-injector Deploy Bot"
|
||||
git config --local user.email "authlib-injector-deploy-bot@yushi.moe"
|
||||
|
||||
mkdir -p "artifact/$build_number"
|
||||
wget -O "artifact/$build_number/$asset_name" '${{ github.event.release.assets[0].browser_download_url }}'
|
||||
|
@ -50,14 +49,12 @@ 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
217
.gitignore
vendored
|
@ -1,197 +1,54 @@
|
|||
# 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
|
||||
## General housekeeping
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
.cache
|
||||
libs/
|
||||
natives/
|
||||
logs/
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
# virtual machine crash logs
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
## Because Macs
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
## Gradle
|
||||
.gradle/
|
||||
build/
|
||||
gradle/
|
||||
gradlew
|
||||
out/
|
||||
output/
|
||||
run/
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
## sublime
|
||||
*.sublime*
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### 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]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.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)
|
||||
## eclipse
|
||||
.classpath
|
||||
.metadata/
|
||||
.project
|
||||
.settings/
|
||||
bin/
|
||||
eclipse/
|
||||
|
||||
### Gradle Patch ###
|
||||
# Java heap dump
|
||||
*.hprof
|
||||
## Intellij IDEA
|
||||
.idea/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/vim,java,gradle,eclipse,visualstudiocode,macos
|
||||
## netbeans
|
||||
nbbuild/
|
||||
nbproject/
|
||||
|
||||
# vim
|
||||
*~
|
||||
|
||||
|
|
14
LICENSE
14
LICENSE
|
@ -659,17 +659,3 @@ 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.
|
||||
|
|
40
README.en.md
40
README.en.md
|
@ -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/actions/workflow/status/yushijinhun/authlib-injector/ci.yml?branch=develop)](https://github.com/yushijinhun/authlib-injector/actions?query=workflow%3ACI)
|
||||
[![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)
|
||||
[![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 17+. The target Java platform version is 8.
|
||||
Dependencies: Gradle, JDK 8+
|
||||
|
||||
Run:
|
||||
```
|
||||
|
@ -77,44 +77,8 @@ 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.
|
||||
|
|
42
README.md
42
README.md
|
@ -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/actions/workflow/status/yushijinhun/authlib-injector/ci.yml?branch=develop)](https://github.com/yushijinhun/authlib-injector/actions?query=workflow%3ACI)
|
||||
[![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)
|
||||
[![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 17+(目标 Java 版本为 8)。
|
||||
构建依赖:Gradle、JDK 8+。
|
||||
|
||||
执行以下命令:
|
||||
```
|
||||
|
@ -37,7 +37,7 @@ gradle
|
|||
|
||||
需要注意的是, authlib-injector 的日志是不会输出到 Minecraft 服务端/客户端的日志文件中的.
|
||||
|
||||
每次启动时, 日志文件都会被清空. 如果有多个进程使用同一个日志文件, 则只有最早启动的会成功打开日志文件.
|
||||
每次启动时,日志文件都会被清空. 如果有多个进程使用同一个日志文件, 则只有最早启动的会成功打开日志文件.
|
||||
|
||||
-Dauthlibinjector.mojangNamespace={default|enabled|disabled}
|
||||
设置是否启用 Mojang 命名空间 (@mojang 后缀).
|
||||
|
@ -85,46 +85,10 @@ 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 虚拟机。
|
||||
|
|
19
build.gradle
19
build.gradle
|
@ -1,6 +1,6 @@
|
|||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'com.palantir.git-version' version '3.0.0'
|
||||
id 'com.github.johnrengelman.shadow' version '6.1.0'
|
||||
id 'com.palantir.git-version' version '0.12.3'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
|
@ -9,14 +9,11 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:9.6'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
|
||||
implementation 'org.ow2.asm:asm:9.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.release = 8
|
||||
options.deprecation = true
|
||||
}
|
||||
sourceCompatibility = 8
|
||||
|
||||
def buildNumber = System.getenv('AI_BUILD_NUMBER')
|
||||
def gitInfo = versionDetails()
|
||||
|
@ -49,12 +46,8 @@ processResources {
|
|||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
archiveClassifier.set(null)
|
||||
classifier = null
|
||||
|
||||
exclude 'META-INF/maven/**'
|
||||
exclude 'module-info.class'
|
||||
|
|
45
docs/Home.md
Normal file
45
docs/Home.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
目录
|
||||
|
||||
- [简介](#%E7%AE%80%E4%BB%8B)
|
||||
- [相关项目](#%E7%9B%B8%E5%85%B3%E9%A1%B9%E7%9B%AE)
|
||||
- [推荐的公共验证服务器](#%E6%8E%A8%E8%8D%90%E7%9A%84%E5%85%AC%E5%85%B1%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8)
|
||||
- [捐助](#%E6%8D%90%E5%8A%A9)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## 简介
|
||||
该项目的目标:
|
||||
* 为修改 Minecraft ,使其使用自定义 Yggdrasil 服务提供工具
|
||||
* 为自定义 Yggdrasil 服务端、使用自定义 Yggdrasil 服务的启动器提供技术规范
|
||||
* 为玩家提供统一的非 Mojang 游戏外登录体验
|
||||
* 玩家可以使用任意实现该规范的启动器,登录任意实现该规范的 Yggdrasil 服务
|
||||
|
||||
该项目会对所有 API 作出详细说明,并且还会定义一些不属于 Yggdrasil 的 API 。这样做是为了最简化指定 Yggdrasil 服务的流程:只需要填写 Yggdrasil 服务对应的 URL ,就可以使用它。
|
||||
|
||||
如果你是 Yggdrasil 服务端开发者或启动器开发者,或是对本项目感兴趣,请 Watch 本项目,以了解规范最新的发展
|
||||
|
||||
> 开发者交流 QQ 群:926979364,Telegram 群:[@authlib_injector](https://t.me/authlib_injector)。欢迎启动器或皮肤站开发者加入。普通用户请勿加群,可能会听不懂。
|
||||
|
||||
## 相关项目
|
||||
* [yggdrasil-mock](https://github.com/yushijinhun/yggdrasil-mock)
|
||||
* Yggdrasil 服务端规范的参考实现,以及 Yggdrasil API 的测试用例
|
||||
* 基于此项目的 Yggdrasil 服务端演示站点:[auth-demo.yushi.moe](https://github.com/yushijinhun/yggdrasil-mock/wiki/演示站点)
|
||||
* [BMCLAPI](https://bmclapidoc.bangbang93.com/#api-Mirrors-Mirrors_authlib_injector)
|
||||
* BMCLAPI 为 authlib-injector 下载提供了一个镜像
|
||||
* [Yggdrasil API for Blessing Skin](https://blessing.netlify.app/yggdrasil-api/)
|
||||
* Blessing Skin 皮肤站的 Yggdrasil 插件
|
||||
* [HMCL](https://github.com/huanghongxun/HMCL)
|
||||
* HMCL v3.x 支持 authlib-injector
|
||||
* [BakaXL](https://www.bakaxl.com/)
|
||||
* BakaXL 3.0 支持 authlib-injector
|
||||
* [LaunchHelper](https://github.com/Codex-in-somnio/LaunchHelper)
|
||||
* 想在 Multicraft 面板服上使用 authlib-injector,可以尝试此项目
|
||||
|
||||
## 推荐的公共验证服务器
|
||||
* [LittleSkin](https://littlesk.in/)
|
||||
* [Ely.by](https://ely.by/)
|
||||
|
||||
## 捐助
|
||||
BMCLAPI 为 authlib-injector 提供了下载镜像站。如果您想要支持 authlib-injector 的开发,您可以[捐助 BMCLAPI](https://bmclapidoc.bangbang93.com/)。
|
842
docs/Yggdrasil-服务端技术规范.md
Normal file
842
docs/Yggdrasil-服务端技术规范.md
Normal file
|
@ -0,0 +1,842 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
目录
|
||||
=================
|
||||
|
||||
- [概述](#%E6%A6%82%E8%BF%B0)
|
||||
- [基本约定](#%E5%9F%BA%E6%9C%AC%E7%BA%A6%E5%AE%9A)
|
||||
- [字符编码](#%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81)
|
||||
- [请求与响应格式](#%E8%AF%B7%E6%B1%82%E4%B8%8E%E5%93%8D%E5%BA%94%E6%A0%BC%E5%BC%8F)
|
||||
- [错误信息格式](#%E9%94%99%E8%AF%AF%E4%BF%A1%E6%81%AF%E6%A0%BC%E5%BC%8F)
|
||||
- [数据格式](#%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F)
|
||||
- [模型](#%E6%A8%A1%E5%9E%8B)
|
||||
- [用户](#%E7%94%A8%E6%88%B7)
|
||||
- [用户信息的序列化](#%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96)
|
||||
- [角色(Profile)](#%E8%A7%92%E8%89%B2profile)
|
||||
- [角色 UUID 的生成](#%E8%A7%92%E8%89%B2-uuid-%E7%9A%84%E7%94%9F%E6%88%90)
|
||||
- [兼容离线验证](#%E5%85%BC%E5%AE%B9%E7%A6%BB%E7%BA%BF%E9%AA%8C%E8%AF%81)
|
||||
- [角色信息的序列化](#%E8%A7%92%E8%89%B2%E4%BF%A1%E6%81%AF%E7%9A%84%E5%BA%8F%E5%88%97%E5%8C%96)
|
||||
- [`textures` 材质信息属性](#textures-%E6%9D%90%E8%B4%A8%E4%BF%A1%E6%81%AF%E5%B1%9E%E6%80%A7)
|
||||
- [`uploadableTextures` 可上传的材质类型](#uploadabletextures-%E5%8F%AF%E4%B8%8A%E4%BC%A0%E7%9A%84%E6%9D%90%E8%B4%A8%E7%B1%BB%E5%9E%8B)
|
||||
- [材质 URL 规范](#%E6%9D%90%E8%B4%A8-url-%E8%A7%84%E8%8C%83)
|
||||
- [用户上传材质的安全性](#%E7%94%A8%E6%88%B7%E4%B8%8A%E4%BC%A0%E6%9D%90%E8%B4%A8%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7)
|
||||
- [令牌(Token)](#%E4%BB%A4%E7%89%8Ctoken)
|
||||
- [令牌的状态](#%E4%BB%A4%E7%89%8C%E7%9A%84%E7%8A%B6%E6%80%81)
|
||||
- [关于暂时失效状态](#%E5%85%B3%E4%BA%8E%E6%9A%82%E6%97%B6%E5%A4%B1%E6%95%88%E7%8A%B6%E6%80%81)
|
||||
- [Yggdrasil API](#yggdrasil-api)
|
||||
- [用户部分](#%E7%94%A8%E6%88%B7%E9%83%A8%E5%88%86)
|
||||
- [登录](#%E7%99%BB%E5%BD%95)
|
||||
- [使用角色名称登录](#%E4%BD%BF%E7%94%A8%E8%A7%92%E8%89%B2%E5%90%8D%E7%A7%B0%E7%99%BB%E5%BD%95)
|
||||
- [刷新](#%E5%88%B7%E6%96%B0)
|
||||
- [验证令牌](#%E9%AA%8C%E8%AF%81%E4%BB%A4%E7%89%8C)
|
||||
- [吊销令牌](#%E5%90%8A%E9%94%80%E4%BB%A4%E7%89%8C)
|
||||
- [登出](#%E7%99%BB%E5%87%BA)
|
||||
- [会话部分](#%E4%BC%9A%E8%AF%9D%E9%83%A8%E5%88%86)
|
||||
- [客户端进入服务器](#%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%BF%9B%E5%85%A5%E6%9C%8D%E5%8A%A1%E5%99%A8)
|
||||
- [服务端验证客户端](#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E9%AA%8C%E8%AF%81%E5%AE%A2%E6%88%B7%E7%AB%AF)
|
||||
- [角色部分](#%E8%A7%92%E8%89%B2%E9%83%A8%E5%88%86)
|
||||
- [查询角色属性](#%E6%9F%A5%E8%AF%A2%E8%A7%92%E8%89%B2%E5%B1%9E%E6%80%A7)
|
||||
- [按名称批量查询角色](#%E6%8C%89%E5%90%8D%E7%A7%B0%E6%89%B9%E9%87%8F%E6%9F%A5%E8%AF%A2%E8%A7%92%E8%89%B2)
|
||||
- [材质上传](#%E6%9D%90%E8%B4%A8%E4%B8%8A%E4%BC%A0)
|
||||
- [PUT 上传材质](#put-%E4%B8%8A%E4%BC%A0%E6%9D%90%E8%B4%A8)
|
||||
- [DELETE 清除材质](#delete-%E6%B8%85%E9%99%A4%E6%9D%90%E8%B4%A8)
|
||||
- [扩展 API](#%E6%89%A9%E5%B1%95-api)
|
||||
- [API 元数据获取](#api-%E5%85%83%E6%95%B0%E6%8D%AE%E8%8E%B7%E5%8F%96)
|
||||
- [材质域名白名单](#%E6%9D%90%E8%B4%A8%E5%9F%9F%E5%90%8D%E7%99%BD%E5%90%8D%E5%8D%95)
|
||||
- [`meta` 中的元数据](#meta-%E4%B8%AD%E7%9A%84%E5%85%83%E6%95%B0%E6%8D%AE)
|
||||
- [服务端基本信息](#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%9F%BA%E6%9C%AC%E4%BF%A1%E6%81%AF)
|
||||
- [服务器网址](#%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%BD%91%E5%9D%80)
|
||||
- [功能选项](#%E5%8A%9F%E8%83%BD%E9%80%89%E9%A1%B9)
|
||||
- [响应示例](#%E5%93%8D%E5%BA%94%E7%A4%BA%E4%BE%8B)
|
||||
- [API 地址指示(ALI)](#api-%E5%9C%B0%E5%9D%80%E6%8C%87%E7%A4%BAali)
|
||||
- [参见](#%E5%8F%82%E8%A7%81)
|
||||
- [参考实现](#%E5%8F%82%E8%80%83%E5%AE%9E%E7%8E%B0)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
# 概述
|
||||
本文旨在为实现 Yggdrasil 服务端提供非官方的技术规范。
|
||||
|
||||
本规范中所描述的服务端行为不一定与 Mojang 服务端的相同。
|
||||
这客观上是因为 Mojang 的服务端是闭源的,我们只能推测其内部逻辑,而所作出的推测难免会与实际存在出入。
|
||||
但事实上只要客户端能够正确理解并处理服务端的响应,那么其行为是否与 Mojang 服务端的相同,也就无关紧要了。
|
||||
|
||||
# 基本约定
|
||||
|
||||
## 字符编码
|
||||
本文中字符编码一律使用 UTF-8。
|
||||
|
||||
## 请求与响应格式
|
||||
若无特殊说明,请求与响应均为 JSON 格式(如果有 body),`Content-Type` 均为 `application/json; charset=utf-8`。
|
||||
|
||||
所有 API 都应该使用 HTTPS 协议。
|
||||
|
||||
## 错误信息格式
|
||||
```javascript
|
||||
{
|
||||
"error":"错误的简要描述(机器可读)",
|
||||
"errorMessage":"错误的详细信息(人类可读)",
|
||||
"cause":"该错误的原因(可选)"
|
||||
}
|
||||
```
|
||||
|
||||
当遇到本文中已说明的异常情况时,返回的错误信息应符合对应的要求。
|
||||
|
||||
下表列举了常见异常情况下的错误信息。除特殊说明外,`cause` 一般不包含。
|
||||
|
||||
**非标准**指由于无法使 Mojang 的 Yggdrasil 服务器触发对应异常,而只能推测该种情况下的错误信息。
|
||||
|
||||
**未定义**指该项并没有明确要求。
|
||||
|
||||
|异常情况|HTTP状态码|Error|Error Message|
|
||||
|--------|----------|-----|------------|
|
||||
|一般 HTTP 异常(非业务异常,如 _Not Found_、_Method Not Allowed_)|_未定义_|_该 HTTP 状态对应的 Reason Phrase(于 [HTTP/1.1](https://tools.ietf.org/html/rfc2616#section-6.1.1) 中定义)_|_未定义_|
|
||||
|令牌无效|403|ForbiddenOperationException|Invalid token.|
|
||||
|密码错误,或短时间内多次登录失败而被暂时禁止登录|403|ForbiddenOperationException|Invalid credentials. Invalid username or password.|
|
||||
|试图向一个已经绑定了角色的令牌指定其要绑定的角色|400|IllegalArgumentException|Access token already has a profile assigned.|
|
||||
|试图向一个令牌绑定不属于其对应用户的角色 _(非标准)_|403|ForbiddenOperationException|_未定义_|
|
||||
|试图使用一个错误的角色加入服务器|403|ForbiddenOperationException|Invalid token.|
|
||||
|
||||
|
||||
## 数据格式
|
||||
我们约定以下数据格式
|
||||
* **无符号 UUID**: 指去掉所有 `-` 字符后的 UUID 字符串
|
||||
|
||||
## 模型
|
||||
|
||||
### 用户
|
||||
一个系统中可以存在若干个用户,用户具有以下属性:
|
||||
* ID
|
||||
* 邮箱
|
||||
* 密码
|
||||
|
||||
其中 ID 为一个无符号 UUID。邮箱可以变更,但需要保证唯一。
|
||||
|
||||
#### 用户信息的序列化
|
||||
用户信息序列化后符合以下格式:
|
||||
```javascript
|
||||
{
|
||||
"id":"用户的 ID",
|
||||
"properties":[ // 用户的属性(数组,每一元素为一个属性)
|
||||
{ // 一项属性
|
||||
"name":"属性的名称",
|
||||
"value":"属性的值",
|
||||
}
|
||||
// ,...(可以有更多)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
用户属性中目前已知的项目如下:
|
||||
|
||||
|名称|值|
|
||||
|--|--------|
|
||||
|preferredLanguage|**(可选)**用户的偏好语言,例如 `en`、`zh_CN`|
|
||||
|
||||
### 角色(Profile)
|
||||
> Mojang 当前不支持多角色,不保证多角色部分内容的正确性。
|
||||
|
||||
角色与账号为多对一关系。一个角色对应 Minecraft 中的一个实体玩家。角色具有以下属性:
|
||||
* UUID
|
||||
* 名称
|
||||
* 材质模型,可选值有:`default`、`slim`
|
||||
* **default**:正常手臂宽度(4px)的皮肤
|
||||
* **slim**:细手臂(3px)的皮肤
|
||||
* 材质
|
||||
* 类型为映射
|
||||
* key 可选值有:SKIN、CAPE
|
||||
* value 类型为 URL
|
||||
|
||||
UUID 和名称均为全局唯一,但名称可变。应避免使用名称作为标识。
|
||||
|
||||
#### 角色 UUID 的生成
|
||||
若不考虑兼容性,角色的 UUID 一般为随机生成([Version 4](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)))。
|
||||
|
||||
但 Minecraft 仅使用 UUID 作为角色标识符,不同 UUID 的角色即使名称相同也被认为是不同的。如果一个 Minecraft 服务器从其他登录系统(正版验证、离线验证或其他)迁移到本登录系统,并且角色的 UUID 发生了变化,则该角色的数据将丢失。为了避免这种情况,必须保证对于同一个角色,本系统生成的 UUID 与其在先前系统中的 UUID 是相同的。
|
||||
|
||||
##### 兼容离线验证
|
||||
若 Minecraft 服务器原先采用的是离线验证,则角色 UUID 是角色名称的一元函数。如果 Yggdrasil 服务端使用此方法生成角色 UUID,就可以实现与离线验证系统之间的双向兼容,即可以在不丢失角色数据的情况下,在离线验证系统和本登录系统之间切换。
|
||||
|
||||
从角色名称计算角色 UUID 的代码如下(Java):
|
||||
```java
|
||||
UUID.nameUUIDFromBytes(("OfflinePlayer:" + characterName).getBytes(StandardCharsets.UTF_8))
|
||||
```
|
||||
|
||||
在其他语言中的实现:
|
||||
* [PHP](https://gist.github.com/games647/2b6a00a8fc21fd3b88375f03c9e2e603)
|
||||
|
||||
#### 角色信息的序列化
|
||||
角色信息序列化后符合以下格式:
|
||||
```javascript
|
||||
{
|
||||
"id":"角色 UUID(无符号)",
|
||||
"name":"角色名称",
|
||||
"properties":[ // 角色的属性(数组,每一元素为一个属性)(仅在特定情况下需要包含)
|
||||
{ // 一项属性
|
||||
"name":"属性的名称",
|
||||
"value":"属性的值",
|
||||
"signature":"属性值的数字签名(仅在特定情况下需要包含)"
|
||||
}
|
||||
// ,...(可以有更多)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
角色属性(`properties`)及数字签名(`signature`)在无特殊说明的情况下不需要包含。
|
||||
|
||||
`signature` 是属性值的数字签名,使用 Base64 编码。签名算法为 SHA1withRSA,见 [PKCS #1](https://www.rfc-editor.org/rfc/rfc2437.txt)。关于签名密钥的详细介绍,见 [签名密钥对](签名密钥对)。
|
||||
|
||||
角色属性中可以包含以下项目:
|
||||
|名称|值|
|
||||
|----|--|
|
||||
|textures|(可选)Base64 编码的 JSON 字符串,包含了角色的材质信息,详见 [§`textures` 材质信息属性](#textures-材质信息属性)。|
|
||||
|uploadableTextures|(可选)该角色可以上传的材质类型,为 authlib-injector 自行规定的属性,详见 [§`uploadableTextures` 可上传的材质类型](#uploadableTextures-可上传的材质类型)|
|
||||
|
||||
#### `textures` 材质信息属性
|
||||
以下为材质信息的格式,将这段 JSON 进行 Base64 编码后,即为 `textures` 角色属性的值。
|
||||
```javascript
|
||||
{
|
||||
"timestamp":该属性值被生成时的时间戳(Java 时间戳格式,即自 1970-01-01 00:00:00 UTC 至今经过的毫秒数),
|
||||
"profileId":"角色 UUID(无符号)",
|
||||
"profileName":"角色名称",
|
||||
"textures":{ // 角色的材质
|
||||
"材质类型(如 SKIN)":{ // 若角色不具有该项材质,则不必包含
|
||||
"url":"材质的 URL",
|
||||
"metadata":{ // 材质的元数据,若没有则不必包含
|
||||
"名称":"值"
|
||||
// ,...(可以有更多)
|
||||
}
|
||||
}
|
||||
// ,...(可以有更多)
|
||||
}
|
||||
}
|
||||
```
|
||||
材质元数据中目前已知的项目有 `model`,其对应该角色的材质模型,取值为 `default` 或 `slim`。
|
||||
|
||||
#### `uploadableTextures` 可上传的材质类型
|
||||
> **注意:** 这一角色属性是由 authlib-injector 文档规定的,Mojang 返回的角色属性是不包含这一项的。Mojang 仅允许用户上传皮肤,不允许上传披风。
|
||||
|
||||
考虑到并非所有验证服务器都允许用户上传皮肤和披风,因此 authlib-injector 规定了 `uploadableTextures` 角色属性,其表示角色可以上传的材质类型。
|
||||
|
||||
该属性的值是一个逗号分隔的列表,包含了可以上传的材质类型。材质类型目前有 `skin` 和 `cape` 两种。
|
||||
|
||||
例如,`uploadableTextures` 属性的值若为 `skin`,则表示可以为该角色上传皮肤,但不能上传披风;值若为 `skin,cape`,则既可以上传皮肤,又可以上传披风。
|
||||
|
||||
如果不存在 `uploadableTextures` 属性,则不能为该角色上传任何类型的材质。
|
||||
|
||||
关于材质上传接口的介绍,请参考 [§材质上传](#材质上传)。
|
||||
|
||||
#### 材质 URL 规范
|
||||
Minecraft 将材质 hash 作为材质的标识。每当客户端下载一个材质后,便会将其缓存在本地,以后若需要相同 hash 的材质,则会直接使用缓存。
|
||||
而这个 hash 并不是由客户端计算的。Yggdrasil 服务端应先计算好材质 hash,将其作为材质 URL 的文件名,即从 URL 最后一个 `/`(不包括)开始一直到结尾的这一段子串。
|
||||
而客户端会直接将 URL 的文件名作为材质的 hash。
|
||||
|
||||
例如下面这个 URL,它所代表的材质的 hash 为 `e051c27e803ba15de78a1d1e83491411dffb6d7fd2886da0a6c34a2161f7ca99`:
|
||||
```
|
||||
https://yggdrasil.example.com/textures/e051c27e803ba15de78a1d1e83491411dffb6d7fd2886da0a6c34a2161f7ca99
|
||||
```
|
||||
|
||||
> 安全警告:
|
||||
> * 材质 URL 响应头中的 `Content-Type` 必须为 `image/png`。若未指定,则存在 MIME Sniffing Attack 的风险。
|
||||
|
||||
由于 PNG 格式图像包含与显示无关的数据,因此即使是图像尺寸与内容完全相同的 PNG 文件,它们的 hash 值也可能不同。
|
||||
为此,需要使用一个仅与图像内容有关的方法来计算材质的 hash。规定这个方法如下:
|
||||
1. 首先创建一个长度为 `(width * height * 4 + 8)` 字节的缓冲区,其中 `width` 和 `height` 为图像的长和宽
|
||||
2. 填充该缓冲区
|
||||
1. `0~3` 字节为 `width`,以大端序存储
|
||||
2. `4~7` 字节为 `height`,以大端序存储
|
||||
3. 对于每一个像素,设其坐标为 `(x, y)`,其首地址 `offset` 为 `((y + x * height) * 4 + 8)`
|
||||
1. 第 `(offset + 0)`、`(offset + 1)`、`(offset + 2)`、`(offset + 3)` 个字节分别为该像素的 Alpha、Red、Green、Blue 分量
|
||||
2. 若 Alpha 分量为 `0x00`(透明),则 RGB 分量皆作为 `0x00` 处理
|
||||
3. 计算以上缓冲区内数据的 `SHA-256`,作为材质的 hash
|
||||
|
||||
> 目前无法确定 Mojang 所使用的 hash 算法,其输出长度(61 个 hex 字符)不属于任何已知 hash 算法。
|
||||
> 如果使用 SHA-256 作为 hash 算法,由于输出长度不同,不会与 Mojang 的 hash 算法发生冲突,因此是可行的。
|
||||
|
||||
<details>
|
||||
<summary>Java 实现示例</summary>
|
||||
|
||||
```java
|
||||
public static String textureHash(BufferedImage img) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
int width = img.getWidth();
|
||||
int height = img.getHeight();
|
||||
byte[] buf = new byte[4096];
|
||||
|
||||
putInt(buf, 0, width); // 0~3: width(big-endian)
|
||||
putInt(buf, 4, height); // 4~7: height(big-endian)
|
||||
int pos = 8;
|
||||
for (int x = 0; x < width; x++) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
// pos+0: alpha
|
||||
// pos+1: red
|
||||
// pos+2: green
|
||||
// pos+3: blue
|
||||
putInt(buf, pos, img.getRGB(x, y));
|
||||
if (buf[pos + 0] == 0) {
|
||||
// the pixel is transparent
|
||||
buf[pos + 1] = buf[pos + 2] = buf[pos + 3] = 0;
|
||||
}
|
||||
pos += 4;
|
||||
if (pos == buf.length) {
|
||||
// buffer is full
|
||||
pos = 0;
|
||||
digest.update(buf, 0, buf.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos > 0) {
|
||||
// flush
|
||||
digest.update(buf, 0, pos);
|
||||
}
|
||||
|
||||
byte[] sha256 = digest.digest();
|
||||
return String.format("%0" + (sha256.length << 1) + "x", new BigInteger(1, sha256)); // to hex
|
||||
}
|
||||
|
||||
// put an int into the array in big-endian
|
||||
private static void putInt(byte[] array, int offset, int x) {
|
||||
array[offset + 0] = (byte) (x >> 24 & 0xff);
|
||||
array[offset + 1] = (byte) (x >> 16 & 0xff);
|
||||
array[offset + 2] = (byte) (x >> 8 & 0xff);
|
||||
array[offset + 3] = (byte) (x >> 0 & 0xff);
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>JavaScript 实现示例</summary>
|
||||
|
||||
> 本实现使用了 [pngjs-nozlib](https://www.npmjs.com/package/pngjs-nozlib)。
|
||||
|
||||
```javascript
|
||||
let crypto = require("crypto");
|
||||
let PNG = require("pngjs-nozlib").PNG;
|
||||
let fs = require("fs");
|
||||
|
||||
function computeTextureHash(image) {
|
||||
const bufSize = 8192;
|
||||
let hash = crypto.createHash("sha256");
|
||||
let buf = Buffer.allocUnsafe(bufSize);
|
||||
let width = image.width;
|
||||
let height = image.height;
|
||||
buf.writeUInt32BE(width, 0);
|
||||
buf.writeUInt32BE(height, 4);
|
||||
let pos = 8;
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
let imgidx = (width * y + x) << 2;
|
||||
let alpha = image.data[imgidx + 3];
|
||||
buf.writeUInt8(alpha, pos + 0);
|
||||
if (alpha === 0) {
|
||||
buf.writeUInt8(0, pos + 1);
|
||||
buf.writeUInt8(0, pos + 2);
|
||||
buf.writeUInt8(0, pos + 3);
|
||||
} else {
|
||||
buf.writeUInt8(image.data[imgidx + 0], pos + 1);
|
||||
buf.writeUInt8(image.data[imgidx + 1], pos + 2);
|
||||
buf.writeUInt8(image.data[imgidx + 2], pos + 3);
|
||||
}
|
||||
pos += 4;
|
||||
if (pos === bufSize) {
|
||||
pos = 0;
|
||||
hash.update(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos > 0) {
|
||||
hash.update(buf.slice(0, pos));
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
console.info(computeTextureHash(PNG.sync.read(fs.readFileSync("texture-hash-test.png"))));
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>测试样例</summary>
|
||||
|
||||
> 样例输入:[texture-hash-test.png](https://raw.githubusercontent.com/wiki/yushijinhun/authlib-injector/texture-hash-test.png)
|
||||
>
|
||||
> 样例输出:`47a4c518f80f94ad8737713e0325a98e1f2647f962b9a646f58cd0bbd5afe683`
|
||||
>
|
||||
> 使用上述方法将图片读入到缓冲区,缓冲区中内容如下:
|
||||
> ```
|
||||
> 00 00 00 02 // 宽度:2
|
||||
> 00 00 00 03 // 高度:3
|
||||
> ff ff 00 00 // 像素 (0,0):红
|
||||
> ff 00 00 ff // 像素 (0,1):蓝
|
||||
> ff ff 00 ff // 像素 (0,2):紫
|
||||
> ff 00 ff 00 // 像素 (1,0):绿
|
||||
> 00 00 00 00 // 像素 (1,1):透明
|
||||
> ff ff ff 00 // 像素 (1,2):黄
|
||||
> ```
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
建议所有 Yggdrasil 服务端实现都应将上述算法作为材质 hash 的计算方法。
|
||||
这样可以确保即使相同的材质来自不同的 Yggdrasil 服务端,它们的 hash 也是相同的,进而避免客户端不必要的重复下载和存储。
|
||||
|
||||
#### 用户上传材质的安全性
|
||||
> 安全警告:
|
||||
> * 若不对用户上传材质进行处理,则**可能导致远程代码执行**
|
||||
> * 在读取材质前,若不先检查图像大小,则**可导致拒绝服务攻击**
|
||||
>
|
||||
> 关于此安全缺陷的详细信息:[未经检查的用户上传材质可能导致远程代码执行 #10](https://github.com/yushijinhun/authlib-injector/issues/10)
|
||||
|
||||
除了位图数据外,PNG 文件还可以存储其他数据。如果 Yggdrasil 服务端不对用户上传的材质进行检查,则攻击者可以在其中藏匿恶意代码,并通过 Yggdrasil 服务端分发到客户端。因此,Yggdrasil 服务端**必须**对用户上传的材质进行处理,除去其中任何与位图无关的数据。具体做法如下:
|
||||
1. 读取该 PNG 文件中图像的大小,如果过大则应拒绝。
|
||||
* 即使是非常小的 PNG 文件也可以存储一幅足以消耗计算机所有内存的图像(即 PNG Bomb),因此切不可在检查图像大小前就将其完整读入。
|
||||
2. 检查图像是否为合法的皮肤/披风材质。
|
||||
* 皮肤的宽高为 64x32 的整数倍或 64x64 的整数倍,披风的宽高为 64x32 的整数倍或 22x17 的整数倍。宽高为 22x17 整数倍的披风并非标准尺寸的披风,服务端需要用透明像素将其宽高补足至 64x32 的整数倍。
|
||||
3. 计算该材质的 hash([计算方法见上](#材质-url-规范)),并将其位图数据保存。
|
||||
* 切不可直接保存用户上传的 PNG 文件。
|
||||
|
||||
> 实现提示:在 Java 中可使用 [`ImageReader.getWidth()`](https://docs.oracle.com/javase/10/docs/api/javax/imageio/ImageReader.html#getWidth(int)) 在不读入整个图像的情况下获取其尺寸。
|
||||
|
||||
### 令牌(Token)
|
||||
令牌与账号为多对一关系。令牌是一种登录凭证,具有时效性。令牌具有以下属性:
|
||||
* accessToken
|
||||
* clientToken
|
||||
* 绑定的角色
|
||||
* 颁发时间
|
||||
|
||||
其中 `accessToken` 和 `clientToken` 为任意字符串(可以是无符号 UUID 或 JWT)。`accessToken` 由服务端随机生成,`clientToken` 由客户端提供。
|
||||
|
||||
介于 `accessToken` 的随机性,它可以被作为主键。而 `clientToken` 不具有唯一性。
|
||||
|
||||
绑定的角色可以为空。它代表了能使用该令牌进行游戏的角色。
|
||||
|
||||
一个用户可以同时有多个令牌,但服务端也应该对令牌数量加以限制。当令牌数量超出限制(如 10 个)时,则应先吊销最旧的令牌,之后再颁发新的令牌。
|
||||
|
||||
#### 令牌的状态
|
||||
令牌有以下三种状态:
|
||||
* **有效**
|
||||
* 处于该状态的令牌可以进行各项操作,如[进服验证](#会话部分)、[刷新](#刷新)。
|
||||
* 新颁发的令牌(即通过[登录](#登录)、[刷新](#刷新)颁发的令牌)处于该状态。
|
||||
* **暂时失效**
|
||||
* 处于该状态的令牌除了进行[刷新操作](#刷新)外,无权进行任何操作。
|
||||
* 当令牌绑定的角色改名后,令牌应被标记为**暂时失效**状态。
|
||||
* 这是为了让启动器刷新令牌,从而获取到新的角色名称。(见 [#40](https://github.com/yushijinhun/authlib-injector/issues/40))
|
||||
* _该状态并不是必须要实现的(详细介绍见下)。_
|
||||
* **无效**
|
||||
* 处于该状态的令牌无权进行任何操作。
|
||||
* 令牌被吊销后处于该状态。这里的吊销包括[显式吊销](#吊销令牌)、[登出](#登出)、[刷新](#刷新)后吊销原令牌、令牌过期。
|
||||
|
||||
令牌的状态只能由有效变为无效,或是由有效变为暂时失效再变为无效,这个过程是不可逆的。
|
||||
刷新操作仅颁发一个新的令牌,并不能使原令牌重新回到有效状态。
|
||||
|
||||
令牌应当有一个过期时限(如 15 天)。当自颁发起所经过的时间超过该时限时,令牌过期。
|
||||
|
||||
##### 关于暂时失效状态
|
||||
Mojang 对暂时失效状态的实现是这样的:
|
||||
对启动器而言,若令牌处于暂时失效状态,则会刷新令牌,获得一个新的处于有效状态的令牌;
|
||||
对 Yggdrasil 服务端而言,仅最后颁发的令牌才是有效的,先前颁发的其它令牌都处于暂时失效状态。
|
||||
|
||||
Mojang 之所以这么做,可能是为了防止用户多地同时登录(仅使最后一个 session 有效)。但事实上,即使服务端没有实现暂时失效状态,启动器的逻辑也是可以正常工作的。
|
||||
|
||||
当然,就算我们要实现暂时失效状态,也并不需要以 Mojang 的实现为范本。只需要启动器能够正确处理,任何实现都是可以的。下面给出一个不同于 Mojang 的实现的例子:
|
||||
> 取一个短于令牌过期时限的时间段作为有效和暂时失效的分界点。若自颁发起经过的时间在该时限内,则令牌有效;若超过该时限,但仍在过期时限内,则令牌暂时失效。
|
||||
>
|
||||
> 这种做法实现了这样的功能:玩家如果经常进行登录操作,除了第一次登录就不需要输入密码了。而当他长时间未登录时则需要重新输入密码。
|
||||
|
||||
|
||||
# Yggdrasil API
|
||||
|
||||
## 用户部分
|
||||
|
||||
### 登录
|
||||
`POST /authserver/authenticate`
|
||||
|
||||
使用密码进行身份验证,并分配一个新的令牌。
|
||||
|
||||
请求格式:
|
||||
```javascript
|
||||
{
|
||||
"username":"邮箱(或其他凭证,详见 §使用角色名称登录)",
|
||||
"password":"密码",
|
||||
"clientToken":"由客户端指定的令牌的 clientToken(可选)",
|
||||
"requestUser":true/false, // 是否在响应中包含用户信息,默认 false
|
||||
"agent":{
|
||||
"name":"Minecraft",
|
||||
"version":1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
若请求中未包含 `clientToken`,服务端应该随机生成一个无符号 UUID 作为 `clientToken`。但需要注意 `clientToken` 可以为任何字符串,即请求中提供任何 `clientToken` 都是可以接受的,不一定要为无符号 UUID。
|
||||
|
||||
对于令牌要绑定的角色:若用户没有任何角色,则为空;若用户仅有一个角色,那么通常绑定到该角色;若用户有多个角色,通常为空,以便客户端进行选择。也就是说如果绑定的角色为空,则需要客户端进行角色选择。
|
||||
|
||||
响应格式:
|
||||
```javascript
|
||||
{
|
||||
"accessToken":"令牌的 accessToken",
|
||||
"clientToken":"令牌的 clientToken",
|
||||
"availableProfiles":[ // 用户可用角色列表
|
||||
// ,... 每一项为一个角色(格式见 §角色信息的序列化)
|
||||
],
|
||||
"selectedProfile":{
|
||||
// ... 绑定的角色,若为空,则不需要包含(格式见 §角色信息的序列化)
|
||||
},
|
||||
"user":{
|
||||
// ... 用户信息(仅当请求中 requestUser 为 true 时包含,格式见 §用户信息的序列化)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**安全提示:** 该 API 可以被用于密码暴力破解,应受到速率限制。限制应针对用户,而不是客户端 IP。
|
||||
|
||||
#### 使用角色名称登录
|
||||
除使用邮箱登录外,验证服务器还可以允许用户使用角色名称登录。要实现这一点,验证服务器需要进行以下工作:
|
||||
* 将 API 元数据中的 `feature.non_email_login` 字段设置为 true。(见 [API 元数据获取§功能选项](#功能选项))
|
||||
* 接受在[登录接口](#登录)中使用角色名称作为 `username` 参数。
|
||||
|
||||
当用户使用角色名称登录时,验证服务器应**自动将令牌绑定到相应角色**,即上文响应中的 `selectedProfile` 应为用户登录时所用的角色。
|
||||
|
||||
这种情况下,如果用户拥有多个角色,那么他可以省去选择角色的操作。考虑到某些程序不支持多角色(例如 Geyser),还可以通过上述方法绕过角色选择。
|
||||
|
||||
### 刷新
|
||||
`POST /authserver/refresh`
|
||||
|
||||
吊销原令牌,并颁发一个新的令牌。
|
||||
|
||||
请求格式:
|
||||
```javascript
|
||||
{
|
||||
"accessToken":"令牌的 accessToken",
|
||||
"clientToken":"令牌的 clientToken(可选)",
|
||||
"requestUser":true/false, // 是否在响应中包含用户信息,默认 false
|
||||
"selectedProfile":{
|
||||
// ... 要选择的角色(可选,格式见 §角色信息的序列化)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当指定 `clientToken` 时,服务端应检查 `accessToken` 和 `clientToken` 是否有效,否则只需要检查 `accessToken`。
|
||||
|
||||
颁发的新令牌的 `clientToken` 应与原令牌的相同。
|
||||
|
||||
如果请求中包含 `selectedProfile`,那么这就是一个选择角色的操作。此操作要求原令牌所绑定的角色为空,而新令牌则将绑定到 `selectedProfile` 所指定的角色上。如果不包含 `selectedProfile`,那么新令牌所绑定的角色和原令牌相同。
|
||||
|
||||
刷新操作在令牌暂时失效时依然可以执行。若请求失败,原令牌依然有效。
|
||||
|
||||
响应格式:
|
||||
```javascript
|
||||
{
|
||||
"accessToken":"新令牌的 accessToken",
|
||||
"clientToken":"新令牌的 clientToken",
|
||||
"selectedProfile":{
|
||||
// ... 新令牌绑定的角色,若为空,则不需要包含(格式见 §角色信息的序列化)
|
||||
},
|
||||
"user":{
|
||||
// ... 用户信息(仅当请求中 requestUser 为 true 时包含,格式见 §用户信息的序列化)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 验证令牌
|
||||
`POST /authserver/validate`
|
||||
|
||||
检验令牌是否有效。
|
||||
|
||||
请求格式:
|
||||
```javascript
|
||||
{
|
||||
"accessToken":"令牌的 accessToken",
|
||||
"clientToken":"令牌的 clientToken(可选)"
|
||||
}
|
||||
```
|
||||
|
||||
当指定 `clientToken` 时,服务端应检查 `accessToken` 和 `clientToken` 是否有效,否则只需要检查 `accessToken` 。
|
||||
|
||||
若令牌有效,服务端应返回 HTTP 状态 `204 No Content`,否则作为令牌无效的异常情况处理。
|
||||
|
||||
### 吊销令牌
|
||||
`POST /authserver/invalidate`
|
||||
|
||||
吊销给定令牌。
|
||||
|
||||
请求格式:
|
||||
```javascript
|
||||
{
|
||||
"accessToken":"令牌的 accessToken",
|
||||
"clientToken":"令牌的 clientToken(可选)"
|
||||
}
|
||||
```
|
||||
|
||||
服务端只需要检查 `accessToken`,即无论 `clientToken` 为何值都不会造成影响。
|
||||
|
||||
无论操作是否成功,服务端应返回 HTTP 状态 `204 No Content`。
|
||||
|
||||
### 登出
|
||||
`POST /authserver/signout`
|
||||
|
||||
吊销用户的所有令牌。
|
||||
|
||||
请求格式:
|
||||
```javascript
|
||||
{
|
||||
"username":"邮箱",
|
||||
"password":"密码"
|
||||
}
|
||||
```
|
||||
|
||||
若操作成功,服务端应返回 HTTP 状态 `204 No Content`。
|
||||
|
||||
**安全提示:** 该 API 也可用于判断密码的正确性,因此应受到和登录 API 一样的速率限制。
|
||||
|
||||
## 会话部分
|
||||
![Minecraft 玩家进服原理](https://raw.githubusercontent.com/wiki/yushijinhun/authlib-injector/mc入服原理.svg?sanitize=true)
|
||||
|
||||
> 上图使用 ProcessOn 绘制,导出为 SVG。[原始图像](https://www.processon.com/view/link/5a7fbbbae4b0812a0f102187)
|
||||
|
||||
该部分用于角色进入服务器时的验证。主要流程如下:
|
||||
|
||||
1. **Minecraft 服务端**和 **Minecraft 客户端**共同生成一段字符串(`serverId`),其可以被认为是随机的
|
||||
2. **Minecraft 客户端**将 `serverId` 及令牌发送给 **Yggdrasil 服务端**(要求令牌有效)
|
||||
3. **Minecraft 服务端**请求 **Yggdrasil 服务端**检查客户端会话的有效性,即客户端是否成功进行第 2 步
|
||||
|
||||
### 客户端进入服务器
|
||||
`POST /sessionserver/session/minecraft/join`
|
||||
|
||||
记录服务端发送给客户端的 `serverId`,以备服务端检查。
|
||||
|
||||
请求格式:
|
||||
```javascript
|
||||
{
|
||||
"accessToken":"令牌的 accessToken",
|
||||
"selectedProfile":"该令牌绑定的角色的 UUID(无符号)",
|
||||
"serverId":"服务端发送给客户端的 serverId"
|
||||
}
|
||||
```
|
||||
|
||||
仅当 `accessToken` 有效,且 `selectedProfile` 与令牌所绑定的角色一致时,操作才成功。
|
||||
|
||||
服务端应记录以下信息:
|
||||
* serverId
|
||||
* accessToken
|
||||
* 发送该请求的客户端 IP
|
||||
|
||||
实现时请注意:以上信息应记录在内存数据库中(如 Redis),且应该设置过期时间(如 30 秒)。
|
||||
介于 `serverId` 的随机性,可以将其作为主键。
|
||||
|
||||
若操作成功,服务端应返回 HTTP 状态 `204 No Content`。
|
||||
|
||||
### 服务端验证客户端
|
||||
`GET /sessionserver/session/minecraft/hasJoined?username={username}&serverId={serverId}&ip={ip}`
|
||||
|
||||
检查客户端会话的有效性,即数据库中是否存在该 `serverId` 的记录,且信息正确。
|
||||
|
||||
请求参数:
|
||||
|
||||
|参数|值|
|
||||
|----|--|
|
||||
|username|角色的名称|
|
||||
|serverId|服务端发送给客户端的 serverId|
|
||||
|ip _(可选)_|Minecraft 服务端获取到的客户端 IP,仅当 [`prevent-proxy-connections`](https://minecraft.gamepedia.com/Server.properties#prevent-proxy-connections) 选项开启时包含|
|
||||
|
||||
`username` 需要与 `serverId` 所对应令牌所绑定的角色的名称相同。
|
||||
|
||||
响应格式:
|
||||
```javascript
|
||||
{
|
||||
// ... 令牌所绑定角色的完整信息(包含角色属性及数字签名,格式见 §角色信息的序列化)
|
||||
}
|
||||
```
|
||||
|
||||
若操作失败,服务端应返回 HTTP 状态 `204 No Content`。
|
||||
|
||||
## 角色部分
|
||||
该部分用于角色信息的查询。
|
||||
|
||||
### 查询角色属性
|
||||
`GET /sessionserver/session/minecraft/profile/{uuid}?unsigned={unsigned}`
|
||||
|
||||
查询指定角色的完整信息(包含角色属性)。
|
||||
|
||||
请求参数:
|
||||
|
||||
|参数|值|
|
||||
|----|--|
|
||||
|uuid|角色的 UUID(无符号)|
|
||||
|unsigned _(可选)_|`true` 或 `false`。是否在响应中**不包含**数字签名,默认为 `true`|
|
||||
|
||||
响应格式:
|
||||
```javascript
|
||||
{
|
||||
// ... 角色信息(包含角色属性。若 unsigned 为 false,还需要包含数字签名。格式见 §角色信息的序列化)
|
||||
}
|
||||
```
|
||||
|
||||
若角色不存在,服务端应返回 HTTP 状态 `204 No Content`。
|
||||
|
||||
### 按名称批量查询角色
|
||||
`POST /api/profiles/minecraft`
|
||||
|
||||
批量查询角色名称所对应的角色。
|
||||
|
||||
请求格式:
|
||||
```javascript
|
||||
[
|
||||
"角色名称"
|
||||
// ,... 还可以有更多
|
||||
]
|
||||
```
|
||||
|
||||
服务端查询各个角色名称所对应的角色信息,并将其包含在响应中。不存在的角色不需要包含。响应中角色信息的先后次序无要求。
|
||||
|
||||
响应格式:
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
// 角色信息(注意:不包含角色属性。格式见 §角色信息的序列化)
|
||||
}
|
||||
// ,...(可以有更多)
|
||||
]
|
||||
```
|
||||
|
||||
**安全提示:** 为防止 CC 攻击,需要为单次查询的角色数目设置最大值,该值至少为 2。
|
||||
|
||||
## 材质上传
|
||||
```
|
||||
PUT /api/user/profile/{uuid}/{textureType}
|
||||
DELETE /api/user/profile/{uuid}/{textureType}
|
||||
```
|
||||
|
||||
设置或清除指定角色的材质。
|
||||
|
||||
> 并非所有角色都可以上传皮肤和披风。要获取当前角色能够上传的材质类型,参见 [§`uploadableTextures` 可上传的材质类型](#uploadableTextures-可上传的材质类型)。
|
||||
|
||||
请求参数:
|
||||
|
||||
|参数|值|
|
||||
|----|--|
|
||||
|uuid|角色的 UUID(无符号)|
|
||||
|textureType|材质类型,可以为 `skin`(皮肤)或 `cape`(披风)|
|
||||
|
||||
请求需要带上 HTTP 头部 `Authorization: Bearer {accessToken}` 进行认证。若未包含 Authorization 头或 accessToken 无效,则返回 `401 Unauthorized`。
|
||||
|
||||
如果操作成功,则返回 `204 No Content`。
|
||||
|
||||
下面分别介绍 PUT 和 DELETE 这两个 HTTP 方法的用法:
|
||||
|
||||
### PUT 上传材质
|
||||
请求的 `Content-Type` 为 `multipart/form-data`,请求载荷由以下部分组成:
|
||||
|名称(name)|内容|
|
||||
|-----------|----|
|
||||
|model |**(仅用于皮肤)** 皮肤的材质模型,可以为 `slim`(细胳膊皮肤)或空字符串(普通皮肤)。|
|
||||
|file |材质图像,`Content-Type` 须为 `image/png`。<br>建议客户端设置 `Content-Disposition` 中的 `filename` 参数为材质图像的文件名,这可以被验证服务器用作材质的备注。|
|
||||
|
||||
如果操作成功,则返回 `204 No Content`。
|
||||
|
||||
### DELETE 清除材质
|
||||
清除材质后,该类型的材质将恢复为默认。
|
||||
|
||||
# 扩展 API
|
||||
以下 API 是为了方便 authlib-injector 进行自动配置而设计的。
|
||||
|
||||
## API 元数据获取
|
||||
`GET /`
|
||||
|
||||
响应格式:
|
||||
```javascript
|
||||
{
|
||||
"meta":{
|
||||
// 服务端的元数据,内容任意
|
||||
},
|
||||
"skinDomains":[ // 材质域名白名单
|
||||
"域名匹配规则 1"
|
||||
// ,...
|
||||
],
|
||||
"signaturePublickey":"用于验证数字签名的公钥"
|
||||
}
|
||||
```
|
||||
|
||||
`signaturePublickey` 是 PEM 格式的公钥,用于验证角色属性的数字签名。其以 `-----BEGIN PUBLIC KEY-----` 开头,以 `-----END PUBLIC KEY-----` 结尾,中间允许出现换行符,但不允许出现其他空白字符(亦允许文末出现换行符)。
|
||||
|
||||
### 材质域名白名单
|
||||
Minecraft 仅会从白名单中的域名下载材质。如果材质 URL 的域名不在白名单中,则会出现 `Textures payload has been tampered with (non-whitelisted domain)` 错误。采用此机制的原因见 [MC-78491](https://bugs.mojang.com/browse/MC-78491)。
|
||||
|
||||
材质白名单默认包含 `.minecraft.net`、`.mojang.com` 两项规则,你可以设置 `skinDomains` 属性以添加额外的白名单规则。规则格式如下:
|
||||
* 如果规则以 `.`(dot)开头,则匹配以这一规则结尾的域名。
|
||||
* 例如 `.example.com` 匹配 `a.example.com`、`b.a.example.com`,**不匹配** `example.com`。
|
||||
* 如果规则**不以** `.`(dot)开头,则匹配的域名须与规则**完全相同**。
|
||||
* 例如 `example.com` 匹配 `example.com`,**不匹配** `a.example.com`、`eexample.com`。
|
||||
|
||||
### `meta` 中的元数据
|
||||
`meta` 中的内容没有强制要求,以下字段均为可选。
|
||||
|
||||
#### 服务端基本信息
|
||||
|Key|Value|
|
||||
|---|-----|
|
||||
|serverName|服务器名称|
|
||||
|implementationName|服务端实现的名称|
|
||||
|implementationVersion|服务端实现的版本|
|
||||
|
||||
#### 服务器网址
|
||||
如果您需要在启动器中展示验证服务器首页地址、注册页面地址等信息,您可以在 `meta` 中添加一个 `links` 字段。
|
||||
|
||||
`links` 字段的类型是对象,其中可以包含:
|
||||
|Key|Value|
|
||||
|---|-----|
|
||||
|homepage|验证服务器首页地址|
|
||||
|register|注册页面地址|
|
||||
|
||||
#### 功能选项
|
||||
> 以下带有 **_(advanced)_** 标注的字段为高级选项,通常情况下**不需要**设置。
|
||||
|
||||
|Key|Value|
|
||||
|---|-----|
|
||||
|feature.non\_email\_login|布尔值,指示验证服务器是否支持使用邮箱之外的凭证登录(如角色名登录),默认为 false。<br>详情见 [§使用角色名称登录](#使用角色名称登录)。|
|
||||
|feature.legacy\_skin\_api|_(advanced)_ 布尔值,指示验证服务器是否支持旧式皮肤 API,即 `GET /skins/MinecraftSkins/{username}.png`。<br>当未指定或值为 false 时,authlib-injector 会使用内建的 HTTP 服务器在本地处理对该 API 的请求;若值为 true,请求将由验证服务器处理。<br>详情见 [README § 参数] 中的 `-Dauthlibinjector.legacySkinPolyfill` 选项。|
|
||||
|feature.no\_mojang\_namespace|_(advanced)_ 布尔值,是否禁用 authlib-injector 的 Mojang 命名空间(@mojang 后缀)功能,默认为 false。<br>详情见 [README § 参数] 中的 `-Dauthlibinjector.mojangNamespace` 选项。|
|
||||
|
||||
[README § 参数]: https://github.com/yushijinhun/authlib-injector#参数
|
||||
|
||||
### 响应示例
|
||||
```javascript
|
||||
{
|
||||
"meta": {
|
||||
"implementationName": "yggdrasil-mock-server",
|
||||
"implementationVersion": "0.0.1",
|
||||
"serverName": "yushijinhun's Example Authentication Server",
|
||||
"links": {
|
||||
"homepage": "https://skin.example.com/",
|
||||
"register": "https://skin.example.com/register"
|
||||
},
|
||||
"feature.non_email_login": true
|
||||
},
|
||||
"skinDomains": [
|
||||
"example.com",
|
||||
".example.com"
|
||||
],
|
||||
"signaturePublickey": "-----BEGIN PUBLIC KEY-----\nMIICIj...(省略)...EAAQ==\n-----END PUBLIC KEY-----\n"
|
||||
}
|
||||
```
|
||||
|
||||
# API 地址指示(ALI)
|
||||
API 地址指示(API Location Indication,简称 ALI)是一个 HTTP 响应头字段 `X-Authlib-Injector-API-Location`,起到服务发现的作用。ALI 的值为相对 URL 或绝对 URL,它指向与当前页面相关联的 Yggdrasil API。
|
||||
|
||||
使用 ALI 后,用户只需输入一个与 Yggdrasil API 相关联的地址即可,不必输入真正的 API 地址。例如,`https://skin.example.com/api/yggdrasil/` 可以被简化为 `skin.example.com`。支持 ALI 的启动器会请求 `(https://)skin.example.com`,识别响应中的 ALI 头字段,并根据它找到真正的 API 地址。
|
||||
|
||||
皮肤站可以在首页,或在全站启用 ALI。启用 ALI 的方法为在 HTTP 响应中添加 `X-Authlib-Injector-API-Location` 头字段,例如:
|
||||
```
|
||||
X-Authlib-Injector-API-Location: /api/yggdrasil/ # 使用相对 URL
|
||||
X-Authlib-Injector-API-Location: https://skin.example.com/api/yggdrasil/ # 亦可使用绝对 URL,支持跨域
|
||||
```
|
||||
|
||||
当一个页面的 ALI 指向其本身时,这个 ALI 会被忽略。
|
||||
|
||||
|
||||
# 参见
|
||||
* [Authentication - wiki.vg](http://wiki.vg/Authentication)
|
||||
* [Mojang API - wiki.vg](http://wiki.vg/Mojang_API)
|
||||
* [Protocol - wiki.vg](http://wiki.vg/Protocol)
|
||||
|
||||
# 参考实现
|
||||
[yggdrasil-mock](https://github.com/yushijinhun/yggdrasil-mock) 为本规范的参考实现。
|
3
docs/_Footer.md
Normal file
3
docs/_Footer.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
[![Creative Commons License](https://i.creativecommons.org/l/by-sa/4.0/88x31.png)](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
<br>
|
||||
This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/).
|
1
docs/mc入服原理.svg
Normal file
1
docs/mc入服原理.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
BIN
docs/texture-hash-test.png
Normal file
BIN
docs/texture-hash-test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 164 B |
26
docs/yggdrasil-server-dnd-example.html
Normal file
26
docs/yggdrasil-server-dnd-example.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>DnD 方式添加 Yggdrasil 服务端演示</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>
|
||||
拖动右侧文本到启动器:
|
||||
<span id="dndLabel" draggable="true" ondragstart="dndLabel_dragstart(event);">example.yggdrasil.yushi.moe</span>
|
||||
</p>
|
||||
|
||||
<script>
|
||||
function dndLabel_dragstart(event) {
|
||||
let yggdrasilApiRoot = "https://example.yggdrasil.yushi.moe/";
|
||||
let uri = "authlib-injector:yggdrasil-server:" + encodeURIComponent(yggdrasilApiRoot);
|
||||
event.dataTransfer.setData("text/plain", uri);
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
239
docs/启动器技术规范.md
Normal file
239
docs/启动器技术规范.md
Normal file
|
@ -0,0 +1,239 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
目录
|
||||
=================
|
||||
|
||||
- [概述](#%E6%A6%82%E8%BF%B0)
|
||||
- [验证服务器](#%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8)
|
||||
- [验证服务器的设置](#%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E8%AE%BE%E7%BD%AE)
|
||||
- [在配置文件中指定](#%E5%9C%A8%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6%E4%B8%AD%E6%8C%87%E5%AE%9A)
|
||||
- [在启动器中输入地址](#%E5%9C%A8%E5%90%AF%E5%8A%A8%E5%99%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E5%9C%B0%E5%9D%80)
|
||||
- [处理 API 地址指示(ALI)](#%E5%A4%84%E7%90%86-api-%E5%9C%B0%E5%9D%80%E6%8C%87%E7%A4%BAali)
|
||||
- [通过拖拽设置](#%E9%80%9A%E8%BF%87%E6%8B%96%E6%8B%BD%E8%AE%BE%E7%BD%AE)
|
||||
- [拖动数据](#%E6%8B%96%E5%8A%A8%E6%95%B0%E6%8D%AE)
|
||||
- [HTML 示例](#html-%E7%A4%BA%E4%BE%8B)
|
||||
- [验证服务器信息的呈现](#%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%BF%A1%E6%81%AF%E7%9A%84%E5%91%88%E7%8E%B0)
|
||||
- [服务器名称的显示](#%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%90%8D%E7%A7%B0%E7%9A%84%E6%98%BE%E7%A4%BA)
|
||||
- [对非 HTTPS 验证服务器的警告](#%E5%AF%B9%E9%9D%9E-https-%E9%AA%8C%E8%AF%81%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E8%AD%A6%E5%91%8A)
|
||||
- [账户](#%E8%B4%A6%E6%88%B7)
|
||||
- [账户信息的存储](#%E8%B4%A6%E6%88%B7%E4%BF%A1%E6%81%AF%E7%9A%84%E5%AD%98%E5%82%A8)
|
||||
- [账户的添加](#%E8%B4%A6%E6%88%B7%E7%9A%84%E6%B7%BB%E5%8A%A0)
|
||||
- [凭证有效性的确认](#%E5%87%AD%E8%AF%81%E6%9C%89%E6%95%88%E6%80%A7%E7%9A%84%E7%A1%AE%E8%AE%A4)
|
||||
- [账户信息的显示](#%E8%B4%A6%E6%88%B7%E4%BF%A1%E6%81%AF%E7%9A%84%E6%98%BE%E7%A4%BA)
|
||||
- [启动游戏](#%E5%90%AF%E5%8A%A8%E6%B8%B8%E6%88%8F)
|
||||
- [下载 authlib-injector](#%E4%B8%8B%E8%BD%BD-authlib-injector)
|
||||
- [配置预获取](#%E9%85%8D%E7%BD%AE%E9%A2%84%E8%8E%B7%E5%8F%96)
|
||||
- [添加启动参数](#%E6%B7%BB%E5%8A%A0%E5%90%AF%E5%8A%A8%E5%8F%82%E6%95%B0)
|
||||
- [配置 authlib-injector](#%E9%85%8D%E7%BD%AE-authlib-injector)
|
||||
- [替换参数模板](#%E6%9B%BF%E6%8D%A2%E5%8F%82%E6%95%B0%E6%A8%A1%E6%9D%BF)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
# 概述
|
||||
本文旨在为启动器实现 authlib-injector 规范提供技术指导。由于该功能需要调用 Yggdrasil API,因此建议您在阅读本文前,先阅读 [Yggdrasil 服务端技术规范](Yggdrasil服务端技术规范)。
|
||||
|
||||
在启动器中,本登录方式可以被称为【外置登录(authlib-injector)】或【authlib-injector 登录】。我们推荐您使用含义更为明确的前者。
|
||||
|
||||
# 验证服务器
|
||||
验证服务器(即 Yggdrasil 服务器)是整个验证系统的核心,所有验证相关的请求都将被发往它。
|
||||
|
||||
为了确定一个验证服务器,启动器应当存储该验证服务器的 API 地址(即 API Root,如 `https://example.com/api/yggdrasil/`)。
|
||||
|
||||
启动器可以仅支持一个验证服务器,也可以支持多个验证服务器。支持多验证服务器就意味着,启动器中可以同时存在多个账户,并且这些账户可以属于不同的验证服务器。
|
||||
|
||||
## 验证服务器的设置
|
||||
设置验证服务器的操作一般由玩家完成,但也存在着服主进行设置、配置文件同启动器和游戏一起分发的情况。下面介绍几种设置验证服务器的途径:
|
||||
|
||||
### 在配置文件中指定
|
||||
启动器可以将 API 地址直接存储于配置文件中,让用户通过修改配置文件的方式设置验证服务器。这种配置方式实现简单,如果你的启动器只用作服务器专用启动器,则可以使用这种配置方式。
|
||||
|
||||
### 在启动器中输入地址
|
||||
在这种配置方式下,用户通过在启动器中输入 URL 来完成验证服务器的设置。这里的 URL 可能是完整的 API 地址(如 `https://example.com/api/yggdrasil/`),也可能是缩略的地址(如 `example.com`)。
|
||||
|
||||
当 URL 未标明协议(HTTPS 或 HTTP)时,我们约定将其自动补全为 **HTTPS** 协议。亦即,`example.com/api/yggdrasil/` 应当被理解为 `https://example.com/api/yggdrasil/`。
|
||||
|
||||
> 出于安全考虑,启动器即使无法通过 HTTPS 协议连接,也**不得**降级到明文的 HTTP 协议。
|
||||
|
||||
同时,authlib-injector 规定了一种服务发现机制,称为 API 地址指示(ALI)。它用于将用户输入的缩略的、不完整的地址,转换为完整的 API 地址。
|
||||
|
||||
#### 处理 API 地址指示(ALI)
|
||||
为了将用户输入的地址解析为真正的 API 地址,启动器需要进行如下操作:
|
||||
1. 如果 URL 缺少协议,则将其补全为 HTTPS 协议。
|
||||
2. 向 URL 发送 GET 请求(跟随 HTTP 重定向)。
|
||||
3. 如果响应包含 ALI 头(HTTP 头部 `X-Authlib-Injector-API-Location`),那么 ALI 指向的 URL 就是 API 地址。
|
||||
* `X-Authlib-Injector-API-Location` 可以是绝对 URL,亦可以是相对 URL。
|
||||
* 如果 ALI 指向其自身,那就意味着当前 URL 就是 API 地址。
|
||||
4. 如果响应不包含 ALI 头部,则默认认为当前 URL 是 API 地址。
|
||||
|
||||
伪代码:
|
||||
```
|
||||
function resolve_api_url(url)
|
||||
response = http_get(url) // follow redirects
|
||||
|
||||
if response.headers["x-authlib-injector-api-location"] exists
|
||||
new_url = to_absolute_url(response.headers["x-authlib-injector-api-location"])
|
||||
if new_url != url
|
||||
return new_url
|
||||
|
||||
// if you are going to fetch the metadata next, 'response' can be reused
|
||||
return url
|
||||
```
|
||||
|
||||
### 通过拖拽设置
|
||||
此方式允许用户通过[鼠标拖拽 (DnD)](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API)来设置验证服务器。
|
||||
|
||||
DnD 源可以是浏览器或者其他应用程序,而 DnD 目标则是启动器。DnD 源需要展示一段文字、图片或其他内容,示意用户将此内容拖入启动器,以添加验证服务端。在此过程中,验证服务器信息从 DnD 源传输到启动器。在完成 DnD 动作后,启动器向用户确认是否要添加此验证服务器。
|
||||
|
||||
#### 拖动数据
|
||||
拖动数据的 MIME 类型为 `text/plain`,内容为一段 URI,其格式如下:
|
||||
|
||||
```
|
||||
authlib-injector:yggdrasil-server:{验证服务端的 API 地址}
|
||||
```
|
||||
其中的 API 地址是 URI 的一个组成,应当被[编码](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent)。
|
||||
|
||||
拖动效果为复制(`copy`)。
|
||||
|
||||
#### HTML 示例
|
||||
> [演示页面](https://rawgit.com/wiki/yushijinhun/authlib-injector/yggdrasil-server-dnd-example.html)
|
||||
|
||||
在需要拖动的 DOM 节点上添加 `draggable="true"`,并处理 `dragstart` 事件:
|
||||
```html
|
||||
<span id="dndLabel" draggable="true" ondragstart="dndLabel_dragstart(event);">example.yggdrasil.yushi.moe</span>
|
||||
```
|
||||
|
||||
```javascript
|
||||
function dndLabel_dragstart(event) {
|
||||
let yggdrasilApiRoot = "https://example.yggdrasil.yushi.moe/";
|
||||
let uri = "authlib-injector:yggdrasil-server:" + encodeURIComponent(yggdrasilApiRoot);
|
||||
event.dataTransfer.setData("text/plain", uri);
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}
|
||||
```
|
||||
|
||||
## 验证服务器信息的呈现
|
||||
通过向 API 地址发送 GET 请求,启动器可以获取到验证服务器的元数据([响应格式](Yggdrasil-服务端技术规范#api-元数据获取)),例如服务器名称。启动器可以利用这些元数据提升用户体验。
|
||||
|
||||
### 服务器名称的显示
|
||||
验证服务器在 `meta` 中的 `serverName` 里指定了验证服务器的名称。当启动器需要向用户显示一个验证服务器时,便可以使用该名称。
|
||||
|
||||
需要注意的是,验证服务器名称可能会出现冲突,因此启动器应当提供查看验证服务器 API 地址的方法。例如,当鼠标悬浮在验证服务器名称上时,启动器在 Tooltip 中显示其 API 地址。
|
||||
|
||||
## 对非 HTTPS 验证服务器的警告
|
||||
当用户尝试设置一个使用明文 HTTP 协议的验证服务器时,启动器应向用户显示醒目警告,告知用户这可能将其信息安全置于危险之中,用户的密码将会被明文传输。
|
||||
|
||||
# 账户
|
||||
一个账户对应了游戏中的一个玩家,用户可以在启动时选择一个账户进行游戏。
|
||||
|
||||
> **账户、用户和角色的关系**:启动器中账户的概念,与验证服务器中用户的概念**并不相同**。与启动器中的账户相对应的,是验证服务器中的角色(Profile)。验证服务器中的用户则是一个或多个角色的所有者,其在启动器中并无对应实体。
|
||||
|
||||
## 账户信息的存储
|
||||
启动器通过以下三个不可变的属性来确定一个账户:
|
||||
* 账户所属验证服务器
|
||||
* 账户的标识(如邮箱)
|
||||
* 通常账户标识即为邮箱。但若验证服务器返回的元数据中 `feature.non_email_login` 字段为 true,则表明验证服务器支持使用邮箱以外的凭证登录,即账户标识可以不是邮箱。此时,启动器**不应当**期望用户输入的账户一定是邮箱,同时应注意措辞(如使用「账户」一词代替「邮箱」一词),以免造成迷惑。(详见 [Yggdrasil 服务端技术规范 § 使用角色名称登录](Yggdrasil-服务端技术规范#使用角色名称登录))
|
||||
* 账户所对应角色的 UUID
|
||||
|
||||
仅当两个账户的以上三个属性都相同时,这两个账户才是相同的,其中一个属性相同并不能代表两账户相同。同一验证服务器上可以存在多个角色;多个角色可以属于同一个用户;不同验证服务器上也可以出现具有相同 UUID 的角色。因此,启动器应**同时**使用这三个属性来标识账户。
|
||||
|
||||
除了以上三个属性外,账户还具有以下属性:
|
||||
* 令牌(accessToken 及 clientToken)
|
||||
* 账户所对应角色的名称
|
||||
* 用户的 ID
|
||||
* 用户的属性
|
||||
|
||||
> 安全警告:
|
||||
> * 记住登录状态记录的是令牌,**不是**用户的密码。密码在任何时候都**不应该**被明文存储。
|
||||
|
||||
以上属性都是可变的。每次登录或刷新操作后,启动器都需要更新存储的账户属性。
|
||||
|
||||
在下面所有的登录和刷新操作中,请求中 `requestUser` 参数都为 `true`,这样启动器便能够即时更新用户 ID 和用户属性。
|
||||
|
||||
## 账户的添加
|
||||
如果用户要添加一个账户,则启动器需要询问用户使用的验证服务器、用户的账户和密码。这里的验证服务器,可以是预先设置好的,可以是用户从验证服务器列表中选择的,也可以是用户即时设置的([见上文](#验证服务器的设置))。
|
||||
|
||||
此后,启动器进行以下操作:
|
||||
1. 调用相应验证服务器的[登录接口](Yggdrasil-服务端技术规范#登录),其中包含用户输入的账户和密码。
|
||||
2. 如果响应中 `selectedProfile` 不为空,则登录成功,使用响应中的信息更新账户属性。流程结束。
|
||||
3. 如果响应中 `availableProfiles` 为空,则用户没有任何角色,触发异常。
|
||||
4. 提示用户从 `availableProfiles` 中选择一个角色。
|
||||
5. 调用[刷新接口](Yggdrasil-服务端技术规范#刷新),其中的令牌为登录操作所返回的令牌,`selectedProfile` 为上一步中用户选择的角色。
|
||||
6. 登录成功,使用刷新响应中的信息更新账户属性。
|
||||
|
||||
## 凭证有效性的确认
|
||||
启动器在使用凭证前(例如启动游戏前),需要确认其有效性。如果凭证失效,则需要用户重新登录。确认凭证有效性的步骤如下:
|
||||
1. 调用[验证令牌接口](Yggdrasil-服务端技术规范#验证令牌),其中包含账户的 accessToken 和 clientToken。
|
||||
2. 如果请求成功,则当前凭证有效,流程结束。否则继续执行。
|
||||
3. 调用[刷新接口](Yggdrasil-服务端技术规范#刷新),其中包含账户的 accessToken 和 clientToken。
|
||||
4. 如果请求成功,则使用刷新响应中的信息更新账户属性,流程结束。否则继续执行。
|
||||
5. 启动器要求用户重新输入密码。
|
||||
6. 调用[登录接口](Yggdrasil-服务端技术规范#登录),其中包含用户的账户和上一步输入的密码。
|
||||
7. 如果登录响应中 `selectedProfile` 不为空,则:
|
||||
1. 如果 `selectedProfile` 中的 `uuid` 与账户对应角色的 UUID 相同,则使用登录响应中的信息更新用户属性。流程结束。
|
||||
2. 触发异常(原账户的角色已不可用)。
|
||||
8. 从 `availableProfiles` 中找出 UUID 与账户对应角色相同的角色。如果没有,则触发异常(原账户的角色已不可用)。
|
||||
9. 调用[刷新接口](Yggdrasil-服务端技术规范#刷新),其中令牌为登录操作返回的令牌,`selectedProfile` 为上一步中所找到的角色。
|
||||
10. 登录成功,使用刷新响应中的信息更新账户属性。
|
||||
|
||||
## 账户信息的显示
|
||||
启动器在显示账户时,除了账户对应角色的名称之外,还应该显示账户所属的验证服务器([见上文](#服务器名称的显示)),防止用户混淆不同验证服务器上的同名角色。
|
||||
|
||||
如果启动器要显示角色皮肤,则可以调用[查询角色属性接口](Yggdrasil-服务端技术规范#查询角色属性)获取角色属性,角色属性中包含了[角色的皮肤信息](Yggdrasil-服务端技术规范#角色信息的序列化)。
|
||||
|
||||
# 启动游戏
|
||||
启动器在启动游戏前需要进行以下工作:
|
||||
1. (若需要)[下载 authlib-injector](#下载-authlib-injector)
|
||||
2. [确认凭证有效性](#凭证有效性的确认)
|
||||
3. [配置预获取](#配置预获取)
|
||||
4. [添加启动参数](#添加启动参数)
|
||||
|
||||
其中第 1、2、3 步可以并行执行,以提升启动速度。
|
||||
|
||||
## 下载 authlib-injector
|
||||
启动器可以自带 authlib-injector.jar,也可以在启动游戏前下载一个(并缓存)。本项目提供了[一个 API](获取-authlib-injector#下载-api) 用于下载 authlib-injector。
|
||||
|
||||
如果你的用户主要在中国大陆,我们推荐你从 [BMCLAPI 镜像](获取-authlib-injector#bmclapi-镜像)下载。
|
||||
|
||||
## 配置预获取
|
||||
在启动前,启动器需要向 API 地址发送 GET 请求,获取 API 元数据。该元数据会在启动时被传入游戏,这样 authlib-injector 就不必直接请求验证服务器,进而能提升启动速度,并防止因网络故障而导致的游戏启动时崩溃。
|
||||
|
||||
## 添加启动参数
|
||||
### 配置 authlib-injector
|
||||
启动器需要添加以下 JVM 参数(应加在主类参数前):
|
||||
1. javaagent 参数:
|
||||
```
|
||||
-javaagent:{authlib-injector.jar 的路径}={验证服务器 API 地址}
|
||||
```
|
||||
2. 配置预获取:
|
||||
```
|
||||
-Dauthlibinjector.yggdrasil.prefetched={Base64 编码的 API 元数据}
|
||||
```
|
||||
|
||||
下面以 example.yggdrasil.yushi.moe 为例:
|
||||
* authlib-injector.jar 位于 `/home/user/.launcher/authlib-injector.jar`。
|
||||
* 验证服务器的 API 地址为 `https://example.yggdrasil.yushi.moe/`。
|
||||
* 向 `https://example.yggdrasil.yushi.moe/` 发送 GET 请求,获取到 API 元数据:
|
||||
```
|
||||
{"skinDomains":["yushi.moe"],"signaturePublickey":...(省略)
|
||||
```
|
||||
对上面的响应进行 Base64 编码,得到:
|
||||
```
|
||||
eyJza2luRG9tYWluc...(省略)
|
||||
```
|
||||
* 故添加的 JVM 参数为:
|
||||
```
|
||||
-javaagent:/home/user/.launcher/authlib-injector.jar=https://example.yggdrasil.yushi.moe/
|
||||
-Dauthlibinjector.yggdrasil.prefetched=eyJza2luRG9tYWluc...(省略)
|
||||
```
|
||||
|
||||
### 替换参数模板
|
||||
游戏版本 JSON 文件(`versions/<version>/<version>.json`)中规定了启动器启动游戏时使用的参数,其中部分与验证相关的参数模板应按下表进行替换:
|
||||
|
||||
|参数模板|替换为|
|
||||
|------|------|
|
||||
|${auth_access_token}|账户的 accessToken|
|
||||
|${auth_session}|账户的 accessToken|
|
||||
|${auth_player_name}|角色的名称|
|
||||
|${auth_uuid}|角色的 UUID(无符号)|
|
||||
|${user_type}|`mojang`|
|
||||
|${user_properties}|用户的属性(JSON 格式)(若启动器不支持,可替换为 `{}`)|
|
84
docs/在-Minecraft-服务端使用-authlib-injector.md
Normal file
84
docs/在-Minecraft-服务端使用-authlib-injector.md
Normal file
|
@ -0,0 +1,84 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
目录
|
||||
=================
|
||||
|
||||
- [获取 authlib-injector](#%E8%8E%B7%E5%8F%96-authlib-injector)
|
||||
- [原版服务端、Spigot 等](#%E5%8E%9F%E7%89%88%E6%9C%8D%E5%8A%A1%E7%AB%AFspigot-%E7%AD%89)
|
||||
- [BungeeCord](#bungeecord)
|
||||
- [调用 Mojang 皮肤](#%E8%B0%83%E7%94%A8-mojang-%E7%9A%AE%E8%82%A4)
|
||||
- [通过代理访问 Mojang](#%E9%80%9A%E8%BF%87%E4%BB%A3%E7%90%86%E8%AE%BF%E9%97%AE-mojang)
|
||||
- [兼容性](#%E5%85%BC%E5%AE%B9%E6%80%A7)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
本文主要介绍如何在 Minecraft 服务端使用 authlib-injector。
|
||||
|
||||
## 获取 authlib-injector
|
||||
|
||||
首先你需要从[此处](https://authlib-injector.yushi.moe/)下载最新版本的 authlib-injector。
|
||||
|
||||
## 原版服务端、Spigot 等
|
||||
|
||||
请将服务端的 `online-mode` 设置为 `true`,然后在其启动命令中添加以下 JVM 参数:
|
||||
|
||||
```
|
||||
-javaagent:{path/to/authlib-injector.jar}={https://your-yggdrasil-api-root.com}
|
||||
```
|
||||
|
||||
- `{path/to/authlib-injector.jar}` 表示你在[上一步](#获取-authlib-injector)中下载的 JAR 文件所在的位置(相对路径、绝对路径皆可)。
|
||||
- `{https://your-yggdrasil-api-root.com}` 表示验证服务器的 URL。
|
||||
|
||||
例如,这是原先的启动命令:
|
||||
|
||||
```
|
||||
java -jar minecraft_server.1.12.2.jar nogui
|
||||
```
|
||||
|
||||
假设:
|
||||
|
||||
- 你下载到的 authlib-injector JAR 文件名为 `authlib-injector.jar`。
|
||||
- 你将其放到了与服务端 JAR `minecraft_server.1.12.2.jar` 相同的目录下。
|
||||
- 验证服务器的 URL 为 `https://example.yggdrasil.yushi.moe`。
|
||||
|
||||
那么添加参数后的命令行应该如下:
|
||||
|
||||
```
|
||||
java -javaagent:authlib-injector.jar=https://example.yggdrasil.yushi.moe -jar minecraft_server.1.12.2.jar nogui
|
||||
```
|
||||
|
||||
## BungeeCord
|
||||
如果使用 BungeeCord,那么在所有服务端上都需要加载 authlib-injector([方法见上](#原版服务端spigot-等)),但应只有 BungeeCord 打开 `online-mode`,后端 MC 服务端应关闭 `online-mode`。
|
||||
|
||||
## 调用 Mojang 皮肤
|
||||
加载 authlib-injector 后,所有皮肤默认都是从指定的验证服务器处获取的。例如:
|
||||
* `/give @p minecraft:skull 1 3 {SkullOwner:"notch"}`
|
||||
* (Citizens2 插件)`/npc skin notch`
|
||||
|
||||
这些命令获取的都是**自定义的验证服务器上**名为 `notch` 的角色的皮肤。
|
||||
|
||||
如果要使用 Mojang 的皮肤,则可以在角色名称后加上 `@mojang`,如:
|
||||
* `/give @p minecraft:skull 1 3 {SkullOwner:"notch@mojang"}`
|
||||
* `/npc skin notch@mojang`
|
||||
|
||||
详细说明见 [README § 参数](https://github.com/yushijinhun/authlib-injector#参数) 中的 `-Dauthlibinjector.mojangNamespace` 选项。
|
||||
|
||||
### 通过代理访问 Mojang
|
||||
调用 Mojang 皮肤的功能需要 MC 服务端能够访问 Mojang API。如果你的服务端要通过代理才能访问 Mojang,那么你可以在启动时添加以下 **JVM 参数**来指定代理:
|
||||
```
|
||||
-Dauthlibinjector.mojangProxy=socks://<host>:<port>
|
||||
```
|
||||
注意:
|
||||
* 只有向 Mojang 查询角色信息时才会使用此代理,材质图像下载不走代理(即使是来自 Mojang 的材质)。
|
||||
* 目前仅支持 SOCKS5。
|
||||
|
||||
## 兼容性
|
||||
一般而言,authlib-injector 兼容绝大多数插件和 Mod。下表列出的是(曾经)存在兼容性问题的插件 / Mod / 服务端:
|
||||
|
||||
|受影响的插件 / Mod / 服务端|受影响的 authlib-injector 版本|备注|
|
||||
|----|---|----|
|
||||
|Citizens2|<=1.1.23|[#27](https://github.com/yushijinhun/authlib-injector/issues/27) [#28](https://github.com/yushijinhun/authlib-injector/pull/28)|
|
||||
|LaunchWrapper|=1.1.24|[#33](https://github.com/yushijinhun/authlib-injector/issues/33)|
|
||||
|ModLauncher|1.1.24, 1.1.25|[#38](https://github.com/yushijinhun/authlib-injector/pull/38)|
|
||||
|Arclight|<=1.1.30|[#80](https://github.com/yushijinhun/authlib-injector/issues/80)|
|
||||
|Geyser (plugin)|<=1.1.31|[#83](https://github.com/yushijinhun/authlib-injector/issues/83)|
|
43
docs/签名密钥对.md
Normal file
43
docs/签名密钥对.md
Normal file
|
@ -0,0 +1,43 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
目录
|
||||
=================
|
||||
|
||||
- [概述](#%E6%A6%82%E8%BF%B0)
|
||||
- [密钥对的生成和处理](#%E5%AF%86%E9%92%A5%E5%AF%B9%E7%9A%84%E7%94%9F%E6%88%90%E5%92%8C%E5%A4%84%E7%90%86)
|
||||
- [生成私钥](#%E7%94%9F%E6%88%90%E7%A7%81%E9%92%A5)
|
||||
- [从私钥生成公钥](#%E4%BB%8E%E7%A7%81%E9%92%A5%E7%94%9F%E6%88%90%E5%85%AC%E9%92%A5)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
# 概述
|
||||
本文主要介绍用于数字签名的密钥对。文中将使用 OpenSSL 对密钥进行操作。
|
||||
|
||||
验证服务器会对以下请求的响应中的角色属性进行[数字签名](Yggdrasil-服务端技术规范#角色信息的序列化):
|
||||
* [服务端验证客户端](Yggdrasil-服务端技术规范#服务端验证客户端)
|
||||
* [查询角色属性](Yggdrasil-服务端技术规范#查询角色属性)(仅当 `unsigned=false` 时才需要)
|
||||
|
||||
验证服务器通过[API 元数据](Yggdrasil-服务端技术规范#api-元数据获取)将公钥公开,以便 authlib-injector 获取。
|
||||
|
||||
注意:验证服务器应避免密钥的变化。如果使用多个服务端实例进行负载均衡,那么它们应该使用同一个密钥。
|
||||
|
||||
# 密钥对的生成和处理
|
||||
下面对 OpenSSL 的调用都是使用标准输入和标准输出进行输入输出的。
|
||||
如果要使用文件,可使用参数 `-in <file>` 和 `-out <file>`。
|
||||
|
||||
## 生成私钥
|
||||
密钥算法为 RSA,推荐长度为 4096 位。
|
||||
|
||||
```
|
||||
openssl genrsa 4096
|
||||
```
|
||||
|
||||
生成的私钥将输出到标准输出。
|
||||
|
||||
## 从私钥生成公钥
|
||||
```
|
||||
openssl rsa -pubout
|
||||
```
|
||||
|
||||
私钥从标准输入读入,公钥将输出到标准输出。
|
||||
|
63
docs/获取-authlib-injector.md
Normal file
63
docs/获取-authlib-injector.md
Normal file
|
@ -0,0 +1,63 @@
|
|||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
目录
|
||||
=================
|
||||
|
||||
- [手动下载](#%E6%89%8B%E5%8A%A8%E4%B8%8B%E8%BD%BD)
|
||||
- [下载 API](#%E4%B8%8B%E8%BD%BD-api)
|
||||
- [获取版本列表](#%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E5%88%97%E8%A1%A8)
|
||||
- [获取特定版本](#%E8%8E%B7%E5%8F%96%E7%89%B9%E5%AE%9A%E7%89%88%E6%9C%AC)
|
||||
- [获取最新版本](#%E8%8E%B7%E5%8F%96%E6%9C%80%E6%96%B0%E7%89%88%E6%9C%AC)
|
||||
- [BMCLAPI 镜像](#bmclapi-%E9%95%9C%E5%83%8F)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
# 手动下载
|
||||
authlib-injector 的最新版本可以直接从 [authlib-injector.yushi.moe](https://authlib-injector.yushi.moe/) 下载。
|
||||
|
||||
# 下载 API
|
||||
authlib-injector 项目提供了一组 API 用于下载 authlib-injector 构件。其 API 入口为 [`https://authlib-injector.yushi.moe/`](https://authlib-injector.yushi.moe/)。
|
||||
|
||||
## 获取版本列表
|
||||
`GET /artifacts.json`
|
||||
|
||||
响应格式:
|
||||
```javascript
|
||||
{
|
||||
"latest_build_number": 最新版本的构建号,
|
||||
"artifacts": [ // 各版本信息
|
||||
{
|
||||
"build_number": 此版本的构建号,
|
||||
"version": "此版本的版本号"
|
||||
}
|
||||
// ,... (可以有更多)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 获取特定版本
|
||||
`GET /artifact/{build_number}.json`
|
||||
|
||||
URL 中的 `{build_number}` 参数代表版本构建号。
|
||||
|
||||
响应格式:
|
||||
```javascript
|
||||
{
|
||||
"build_number": 此版本的构建号,
|
||||
"version": "此版本的版本号",
|
||||
"download_url": "此版本的 authlib-injector 的下载地址",
|
||||
"checksums": { // 校验和
|
||||
"sha256": "SHA-256 校验和"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取最新版本
|
||||
`GET /artifact/latest.json`
|
||||
|
||||
响应格式[同上](#获取特定版本)。如果你只需要获取最新版本,使用此 API 足矣。
|
||||
|
||||
## BMCLAPI 镜像
|
||||
> 使用 BMCLAPI 时请遵守 [BMCLAPI 的协议](https://bmclapidoc.bangbang93.com/#api-_)。
|
||||
|
||||
BMCLAPI 为本下载 API 提供了一个[镜像](https://bmclapidoc.bangbang93.com/#api-Mirrors-Mirrors_authlib_injector),其入口为 [`https://bmclapi2.bangbang93.com/mirrors/authlib-injector/`](https://bmclapi2.bangbang93.com/mirrors/authlib-injector/)。
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,5 +0,0 @@
|
|||
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
234
gradlew
vendored
|
@ -1,234 +0,0 @@
|
|||
#!/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
89
gradlew.bat
vendored
|
@ -1,89 +0,0 @@
|
|||
@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
|
14
license-header.txt
Normal file
14
license-header.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
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/>.
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2023 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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,33 +42,23 @@ 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;
|
||||
|
@ -249,18 +239,6 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -268,7 +246,7 @@ public final class AuthlibInjector {
|
|||
URLProcessor urlProcessor = new URLProcessor(createFilters(config), new DefaultURLRedirector(config));
|
||||
|
||||
ClassTransformer transformer = new ClassTransformer();
|
||||
transformer.setIgnores(Config.ignoredPackages);
|
||||
transformer.ignores.addAll(Config.ignoredPackages);
|
||||
|
||||
if (Config.dumpClass) {
|
||||
transformer.listeners.add(new DumpClassListener(Paths.get("").toAbsolutePath()));
|
||||
|
@ -281,25 +259,12 @@ 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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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,11 +59,7 @@ 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");
|
||||
|
@ -108,6 +104,34 @@ 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() {
|
||||
|
@ -178,11 +202,7 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,55 +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.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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,6 @@ 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
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2024 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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,15 +49,12 @@ public class QueryUUIDsFilter implements URLFilter {
|
|||
|
||||
@Override
|
||||
public boolean canHandle(String domain) {
|
||||
return domain.equals("api.mojang.com") || domain.equals("api.minecraftservices.com");
|
||||
return domain.equals("api.mojang.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")) ||
|
||||
(domain.equals("api.minecraftservices.com") && path.equals("/minecraft/profile/lookup/bulk/byname") && session.getMethod().equals("POST"))
|
||||
) {
|
||||
if (domain.equals("api.mojang.com") && path.equals("/profiles/minecraft") && session.getMethod().equals("POST")) {
|
||||
Set<String> request = new LinkedHashSet<>();
|
||||
asJsonArray(parseJson(asString(asBytes(session.getInputStream()))))
|
||||
.forEach(element -> request.add(asJsonString(element)));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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 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;
|
||||
|
@ -68,10 +67,6 @@ 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();
|
||||
|
@ -103,7 +98,6 @@ public class URLProcessor {
|
|||
return redirector.redirect(domain, path);
|
||||
}
|
||||
|
||||
private DebugApiEndpoint debugApi = new DebugApiEndpoint();
|
||||
private volatile NanoHTTPD httpd;
|
||||
private final Object httpdLock = new Object();
|
||||
|
||||
|
@ -123,13 +117,9 @@ public class URLProcessor {
|
|||
}
|
||||
|
||||
private NanoHTTPD createHttpd() {
|
||||
return new NanoHTTPD("127.0.0.1", Config.httpdPort) {
|
||||
return new NanoHTTPD("127.0.0.1", 0) {
|
||||
@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");
|
||||
|
@ -188,7 +178,7 @@ public class URLProcessor {
|
|||
conn.setDoOutput(clientIn != null);
|
||||
requestHeaders.forEach(conn::setRequestProperty);
|
||||
|
||||
if (clientIn != null && !method.equalsIgnoreCase("GET") && !method.equalsIgnoreCase("HEAD")) {
|
||||
if (clientIn != null) {
|
||||
try (OutputStream upstreamOut = conn.getOutputStream()) {
|
||||
transfer(clientIn, upstreamOut);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2021 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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,18 +206,16 @@ public class Response implements Closeable {
|
|||
|
||||
protected long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, long defaultSize) {
|
||||
String contentLengthString = getHeader("content-length");
|
||||
if (contentLengthString == null) {
|
||||
pw.print("Content-Length: " + defaultSize + "\r\n");
|
||||
return defaultSize;
|
||||
} else {
|
||||
long size = defaultSize;
|
||||
long size = defaultSize;
|
||||
if (contentLengthString != null) {
|
||||
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 {
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 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
|
||||
|
@ -16,42 +16,17 @@
|
|||
*/
|
||||
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;
|
||||
|
||||
final class CallbackSupport {
|
||||
public final class CallbackSupport {
|
||||
private CallbackSupport() {
|
||||
}
|
||||
|
||||
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;";
|
||||
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 Method findCallbackMethod(Class<?> owner, String methodName) {
|
||||
for (Method method : owner.getDeclaredMethods()) {
|
||||
|
@ -65,123 +40,11 @@ final class CallbackSupport {
|
|||
throw new IllegalArgumentException("No such method: " + methodName);
|
||||
}
|
||||
|
||||
static void callWithInvokeDynamic(MethodVisitor mv, Class<?> owner, String methodName, TransformContext ctx) {
|
||||
public static void invoke(TransformContext ctx, MethodVisitor mv, Class<?> owner, String methodName) {
|
||||
ctx.requireMinimumClassVersion(50);
|
||||
ctx.upgradeClassVersion(51);
|
||||
|
||||
String descriptor = Type.getMethodDescriptor(findCallbackMethod(owner, methodName));
|
||||
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();
|
||||
mv.visitInvokeDynamicInsn(methodName, descriptor, ctx.acquireCallbackMetafactory(), owner.getName());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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,91 +21,83 @@ 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.ACC_INTERFACE;
|
||||
import static org.objectweb.asm.Opcodes.ASM9;
|
||||
import static org.objectweb.asm.Opcodes.H_INVOKESTATIC;
|
||||
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.MethodVisitor;
|
||||
import org.objectweb.asm.Handle;
|
||||
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 PerformanceMetrics performanceMetrics = new PerformanceMetrics();
|
||||
private String[] ignores = new String[0];
|
||||
public final Set<String> ignores = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private class TransformHandle {
|
||||
private static class TransformContextImpl implements TransformContext {
|
||||
|
||||
private class TransformContextImpl implements TransformContext {
|
||||
private final String className;
|
||||
|
||||
public boolean modifiedMark;
|
||||
public boolean callbackMetafactoryRequested = false;
|
||||
public boolean modifiedMark;
|
||||
public int minVersionMark = -1;
|
||||
public int upgradedVersionMark = -1;
|
||||
public boolean callbackMetafactoryRequested = false;
|
||||
|
||||
@Override
|
||||
public void markModified() {
|
||||
modifiedMark = true;
|
||||
}
|
||||
public TransformContextImpl(String className) {
|
||||
this.className = className;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getStringConstants() {
|
||||
return TransformHandle.this.getStringConstants();
|
||||
}
|
||||
@Override
|
||||
public void markModified() {
|
||||
modifiedMark = true;
|
||||
}
|
||||
|
||||
@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 requireMinimumClassVersion(int version) {
|
||||
if (this.minVersionMark < version) {
|
||||
this.minVersionMark = version;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
|
@ -113,143 +105,51 @@ public class ClassTransformer implements ClassFileTransformer {
|
|||
this.classLoader = classLoader;
|
||||
}
|
||||
|
||||
private ClassReader getClassReader() {
|
||||
if (cachedClassReader == null)
|
||||
cachedClassReader = new ClassReader(classBuffer);
|
||||
return cachedClassReader;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
public void accept(TransformUnit unit) {
|
||||
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
TransformContextImpl ctx = new TransformContextImpl(className);
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
};
|
||||
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() {
|
||||
|
@ -265,43 +165,22 @@ 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 ignore : ignores) {
|
||||
if (className.startsWith(ignore)) {
|
||||
for (String prefix : ignores) {
|
||||
if (className.startsWith(prefix)) {
|
||||
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);
|
||||
TransformUnit[] unitsArray = units.toArray(new TransformUnit[0]);
|
||||
handle.accept(unitsArray);
|
||||
units.forEach(handle::accept);
|
||||
listeners.forEach(it -> it.onClassLoading(loader, className, handle.getFinalResult(), handle.getAppliedTransformers()));
|
||||
|
||||
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);
|
||||
|
@ -309,25 +188,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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,19 +16,8 @@
|
|||
*/
|
||||
package moe.yushi.authlibinjector.transform;
|
||||
|
||||
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; }
|
||||
class ClassVersionException extends RuntimeException {
|
||||
public ClassVersionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 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
|
||||
|
@ -19,24 +19,12 @@ 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
|
||||
|
@ -57,21 +45,6 @@ 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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,22 +16,15 @@
|
|||
*/
|
||||
package moe.yushi.authlibinjector.transform;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Handle;
|
||||
|
||||
public interface TransformContext {
|
||||
|
||||
String getClassName();
|
||||
|
||||
boolean isInterface();
|
||||
|
||||
void markModified();
|
||||
|
||||
List<String> getStringConstants();
|
||||
void requireMinimumClassVersion(int version);
|
||||
|
||||
void invokeCallback(MethodVisitor mv, Class<?> owner, String methodName);
|
||||
void upgradeClassVersion(int version);
|
||||
|
||||
void addGeneratedMethod(String name, Consumer<ClassVisitor> generator);
|
||||
Handle acquireCallbackMetafactory();
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2023 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
|
||||
|
@ -37,6 +37,7 @@ 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;
|
||||
|
||||
|
@ -128,18 +129,7 @@ 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));
|
||||
|
@ -242,7 +232,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);
|
||||
ctx.invokeCallback(mv, AuthlibLogInterceptor.class, "onClassLoading");
|
||||
CallbackSupport.invoke(ctx, mv, AuthlibLogInterceptor.class, "onClassLoading");
|
||||
ctx.markModified();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 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
|
||||
|
@ -33,6 +33,7 @@ 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;
|
||||
|
@ -119,7 +120,7 @@ public class MC52974_1710Workaround {
|
|||
if (opcode == ARETURN) {
|
||||
ctx.markModified();
|
||||
super.visitInsn(DUP);
|
||||
ctx.invokeCallback(mv, MC52974_1710Workaround.class, "markGameProfile");
|
||||
CallbackSupport.invoke(ctx, mv, MC52974_1710Workaround.class, "markGameProfile");
|
||||
}
|
||||
super.visitInsn(opcode);
|
||||
}
|
||||
|
@ -162,7 +163,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);
|
||||
ctx.invokeCallback(mv, MC52974_1710Workaround.class, "accessGameProfile");
|
||||
CallbackSupport.invoke(ctx, mv, MC52974_1710Workaround.class, "accessGameProfile");
|
||||
super.visitTypeInsn(CHECKCAST, "com/mojang/authlib/GameProfile");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 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
|
||||
|
@ -31,6 +31,7 @@ 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;
|
||||
|
||||
|
@ -50,7 +51,7 @@ public class MainArgumentsTransformer implements TransformUnit {
|
|||
ctx.markModified();
|
||||
|
||||
super.visitVarInsn(ALOAD, 0);
|
||||
ctx.invokeCallback(mv, MainArgumentsTransformer.class, "processMainArguments");
|
||||
CallbackSupport.invoke(ctx, mv, MainArgumentsTransformer.class, "processMainArguments");
|
||||
super.visitVarInsn(ASTORE, 0);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2023 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
|
||||
|
@ -27,6 +27,7 @@ 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;
|
||||
|
||||
|
@ -49,12 +50,6 @@ 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() {
|
||||
|
@ -63,7 +58,6 @@ public class SkinWhitelistTransformUnit implements TransformUnit {
|
|||
|
||||
@CallbackMethod
|
||||
public static boolean isWhitelistedDomain(String url) {
|
||||
System.out.println(url);
|
||||
String domain;
|
||||
try {
|
||||
domain = new URI(url).getHost();
|
||||
|
@ -71,12 +65,6 @@ 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;
|
||||
|
@ -92,18 +80,17 @@ 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) || "com.mojang.authlib.yggdrasil.TextureUrlChecker".equals(className)) {
|
||||
if ("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService".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) || "isAllowedTextureDomain".equals(name)) &&
|
||||
"(Ljava/lang/String;)Z".equals(desc)) {
|
||||
if ("isWhitelistedDomain".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);
|
||||
ctx.invokeCallback(mv, SkinWhitelistTransformUnit.class, "isWhitelistedDomain");
|
||||
CallbackSupport.invoke(ctx, mv, SkinWhitelistTransformUnit.class, "isWhitelistedDomain");
|
||||
mv.visitInsn(IRETURN);
|
||||
mv.visitMaxs(-1, -1);
|
||||
mv.visitEnd();
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2023 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
|
||||
|
@ -16,206 +16,62 @@
|
|||
*/
|
||||
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.IRETURN;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import static org.objectweb.asm.Opcodes.H_INVOKEVIRTUAL;
|
||||
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
|
||||
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<>();
|
||||
|
||||
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;
|
||||
public static boolean verifyPropertySignature(Object property, PublicKey mojangKey, MethodHandle verifyAction) throws Throwable {
|
||||
if ((boolean) verifyAction.invoke(property, mojangKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[] sig = Base64.getDecoder().decode(base64Signature);
|
||||
byte[] data = propertyValue.getBytes();
|
||||
|
||||
for (PublicKey customKey : PUBLIC_KEYS) {
|
||||
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) {
|
||||
if ((boolean) verifyAction.invoke(property, customKey)) {
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ClassVisitor> transform(ClassLoader classLoader, String className, ClassVisitor writer, TransformContext ctx) {
|
||||
if ("com.mojang.authlib.properties.Property".equals(className)) {
|
||||
if ("com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService".equals(className)) {
|
||||
return Optional.of(new ClassVisitor(ASM9, writer) {
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
} 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();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 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
|
||||
|
@ -29,14 +29,11 @@ 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();
|
||||
|
||||
|
@ -86,12 +83,13 @@ public final class Logging {
|
|||
log += sw.toString();
|
||||
}
|
||||
// remove control characters to prevent messing up the console
|
||||
log = CONTROL_CHARACTERS_FILTER.matcher(log).replaceAll("");
|
||||
log = log.replaceAll("[\\p{Cc}&&[^\r\n\t]]", "");
|
||||
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.
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2019 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,14 +18,15 @@ 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.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
public class ChunkedInputStreamTest {
|
||||
|
@ -75,99 +76,99 @@ public class ChunkedInputStreamTest {
|
|||
assertEquals(underlying.read(), -1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = EOFException.class)
|
||||
public void testReadEOF1() throws IOException {
|
||||
byte[] data = ("a").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(EOFException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = EOFException.class)
|
||||
public void testReadEOF2() throws IOException {
|
||||
byte[] data = ("a\r").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(EOFException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = EOFException.class)
|
||||
public void testReadEOF3() throws IOException {
|
||||
byte[] data = ("a\r\n").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(EOFException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = EOFException.class)
|
||||
public void testReadEOF4() throws IOException {
|
||||
byte[] data = ("a\r\nabc").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(EOFException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = EOFException.class)
|
||||
public void testReadEOF5() throws IOException {
|
||||
byte[] data = ("a\r\n123456789a\r").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(EOFException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = EOFException.class)
|
||||
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);
|
||||
assertThrows(EOFException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = EOFException.class)
|
||||
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);
|
||||
assertThrows(EOFException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IOException.class)
|
||||
public void testBadIn1() throws IOException {
|
||||
byte[] data = ("-1").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(IOException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IOException.class)
|
||||
public void testBadIn2() throws IOException {
|
||||
byte[] data = ("a\ra").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(IOException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IOException.class)
|
||||
public void testBadIn3() throws IOException {
|
||||
byte[] data = ("a\r\n123456789aa").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(IOException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IOException.class)
|
||||
public void testBadIn4() throws IOException {
|
||||
byte[] data = ("a\r\n123456789a\ra").getBytes(US_ASCII);
|
||||
ByteArrayInputStream underlying = new ByteArrayInputStream(data);
|
||||
InputStream in = new ChunkedInputStream(underlying);
|
||||
assertThrows(IOException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IOException.class)
|
||||
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);
|
||||
assertThrows(IOException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2019 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,15 +17,16 @@
|
|||
package moe.yushi.authlibinjector.internal.fi.iki.elonen;
|
||||
|
||||
import static moe.yushi.authlibinjector.util.IOUtils.asBytes;
|
||||
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 static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
public class FixedLengthInputStreamTest {
|
||||
|
@ -57,10 +58,10 @@ public class FixedLengthInputStreamTest {
|
|||
assertEquals(underlying.read(), 0x11);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = EOFException.class)
|
||||
public void testReadEOF() throws IOException {
|
||||
byte[] data = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55 };
|
||||
InputStream in = new FixedLengthInputStream(new ByteArrayInputStream(data), 6);
|
||||
assertThrows(EOFException.class, () -> asBytes(in));
|
||||
asBytes(in);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.Test;
|
||||
import moe.yushi.authlibinjector.APIMetadata;
|
||||
import moe.yushi.authlibinjector.httpd.DefaultURLRedirector;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2019 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,8 @@
|
|||
package moe.yushi.authlibinjector.test;
|
||||
|
||||
import static moe.yushi.authlibinjector.util.KeyUtils.decodePEMPublicKey;
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import org.junit.Test;
|
||||
|
||||
public class KeyUtilsTest {
|
||||
|
||||
|
@ -35,16 +34,14 @@ public class KeyUtilsTest {
|
|||
decodePEMPublicKey("-----BEGIN PUBLIC KEY-----\nf\n39/fw==\n-----END PUBLIC KEY-----\n"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDecodePublicKey3() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> decodePEMPublicKey("-----BEGIN PUBLIC KEY----- f39/fw== -----END PUBLIC KEY-----"));
|
||||
decodePEMPublicKey("-----BEGIN PUBLIC KEY----- f39/fw== -----END PUBLIC KEY-----");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDecodePublicKey4() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> decodePEMPublicKey("-----BEGIN PUBLIC KEY-----f39/fw==-----END NOT A PUBLIC KEY-----"));
|
||||
decodePEMPublicKey("-----BEGIN PUBLIC KEY-----f39/fw==-----END NOT A PUBLIC KEY-----");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2020 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.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SkinWhitelistTest {
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Haowei Wen <yushijinhun@gmail.com> and contributors
|
||||
* Copyright (C) 2019 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,11 @@
|
|||
package moe.yushi.authlibinjector.test;
|
||||
|
||||
import static moe.yushi.authlibinjector.transform.support.MainArgumentsTransformer.inferVersionSeries;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class VersionSeriesDetectTest {
|
||||
|
||||
|
|
Loading…
Reference in a new issue