Compare commits

..

1 commit

Author SHA1 Message Date
Haowei Wen 999ff99751
copy wiki pages to docs/
wiki revision: f583a9f3fce652bfd198c1c80cbb15d794ea75fb
2021-03-06 22:01:29 +08:00
60 changed files with 1803 additions and 2128 deletions

View file

@ -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/*

View file

@ -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"

View file

@ -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
View file

@ -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
View file

@ -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.

View file

@ -3,7 +3,7 @@
# authlib-injector
[![latest release](https://img.shields.io/github/v/tag/yushijinhun/authlib-injector?color=yellow&include_prereleases&label=version&sort=semver&style=flat-square)](https://github.com/yushijinhun/authlib-injector/releases)
[![ci status](https://img.shields.io/github/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.

View file

@ -3,7 +3,7 @@
# authlib-injector
[![latest release](https://img.shields.io/github/v/tag/yushijinhun/authlib-injector?color=yellow&include_prereleases&label=version&sort=semver&style=flat-square)](https://github.com/yushijinhun/authlib-injector/releases)
[![ci status](https://img.shields.io/github/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 虚拟机。

View file

@ -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
View 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 群926979364Telegram 群:[@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/)。

View 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
View 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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

View 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>

View 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 格式)(若启动器不支持,可替换为 `{}`|

View 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
View 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
```
私钥从标准输入读入,公钥将输出到标准输出。

View 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/)。

Binary file not shown.

View file

@ -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
View file

@ -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
View file

@ -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
View 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/>.

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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)));

View file

@ -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);
}

View file

@ -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 {

View file

@ -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";
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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";
}
}

View file

@ -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);
}
};
}
});

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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();
}
};

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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");
}
}

View file

@ -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);
}
};

View file

@ -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";
}
}

View file

@ -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();

View file

@ -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";
}
}

View file

@ -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";
}
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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-----");
}
}

View file

@ -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 {

View file

@ -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 {