71 Commits

Author SHA1 Message Date
a7833a5ca0 Fix plugin constructor visibility level 2023-12-17 17:19:43 +08:00
497d13ef97 Update to build action 2023-12-17 14:20:45 +08:00
67b34e73b4 Update to support Lunar Core 2023-12-17 14:12:18 +08:00
58cd171296 Update to support Lunar Core 2023-12-17 14:08:42 +08:00
896b802bff Update README_en-US.md 2023-11-22 20:44:26 +08:00
4c4840be36 Update README.md 2023-11-22 20:42:04 +08:00
03663406de Update API Version to 2 2023-09-03 10:07:51 +08:00
de765c575e Fix console multi-line commands 2023-09-02 20:26:18 +08:00
f8a4e6f205 Fix empty command 2023-09-02 18:43:27 +08:00
6279ed2982 Update version to v1.7.0 2023-09-02 18:39:18 +08:00
1b86226951 Support multi-line commands
Update auto-generate console token
2023-09-02 18:38:59 +08:00
e9e2805738 Merge pull request #36 from JDDKCN/master
Update plugin.json
2023-09-01 13:44:08 +08:00
剧毒的KCN
d5162a6eac Update plugin.json 2023-09-01 13:41:08 +08:00
54e63d300d Fix verify api 2023-06-04 20:32:00 +08:00
a084d39b02 Update version to v1.6.1 2023-06-04 20:05:50 +08:00
6c19c09c00 Fix deserialization formatting error (#185) 2023-06-04 20:05:32 +08:00
c6e0b51ea6 Fix deserialization formatting error 2023-06-03 00:27:34 +08:00
bcb88740f1 Upgrade to 1.6.0 2023-06-03 00:11:12 +08:00
290c4fbf8c DON'T LOCK PLAYER! 2023-05-21 21:41:37 +08:00
564f4d1e56 Fix CI branch 2023-05-21 21:14:41 +08:00
66fb25aa9b Add url to enable log 2023-05-21 21:04:02 +08:00
c3c2ed08a7 Update player command response handler 2023-05-21 20:51:12 +08:00
82477321d1 Capture Request Exception 2023-05-21 19:50:23 +08:00
f19c4b8e77 Update version to v1.5.2 2023-04-15 19:11:10 +08:00
3f1ecfe8a6 Fix incomplete multiple lines reply issue (fix #28) 2023-04-15 19:10:37 +08:00
止语
79564ff41c 添加新的QQ插件 (#26) 2023-03-18 09:57:34 +08:00
ded44804d4 Fix JsonUtils compatibility issue
Fix locking player object when executing command issue
2023-02-25 16:33:42 +08:00
4b8eb490f5 Remove version prefix dev 2023-02-18 15:49:46 +08:00
70261df520 Fix deprecated method JsonUtils.loadToClass 2023-02-18 15:47:51 +08:00
5c2be0e776 Update version to v1.5.1 2023-02-18 15:46:01 +08:00
72948121d6 Update README_en-US.md 2022-10-27 11:26:23 +08:00
42c748ad2e Update README.md 2022-10-27 11:24:55 +08:00
1d03d3e476 Update version to v1.5.0 2022-10-06 13:27:17 +08:00
ef885af137 Impl token persistence (#19) 2022-10-06 13:27:04 +08:00
b70b73667d Add author @方块君 2022-09-02 20:20:47 +08:00
623856ca99 Update version to v1.4.0
Updated to be compatible with new http server
Add all code headers
2022-09-02 20:18:31 +08:00
方块君
b3bc0f051a Fix send code failed 2022-07-26 23:39:52 +08:00
方块君
1439139404 Update README_en-US.md 2022-07-26 20:49:52 +08:00
方块君
2799a06312 Add multi server deployment tutorial 2022-07-26 20:47:25 +08:00
9fe473d6cc Update version to v1.3.0 2022-07-26 20:19:03 +08:00
d2499bad5f Merge pull request #12 from 577fkj/master
Add multi server support
2022-07-26 20:08:28 +08:00
方块君
6db7c89196 Update build.yml 2022-07-26 19:52:13 +08:00
方块君
dc5cbcc981 Update build.yml 2022-07-26 19:45:07 +08:00
方块君
3ed8e233bc Update README.md 2022-07-26 19:30:31 +08:00
方块君
4712e2cd73 Fix run permission 2022-07-26 19:23:10 +08:00
方块君
6aa19ce860 Add github actions 2022-07-26 19:20:42 +08:00
方块君
4d08acd084 Add multi server run console command 2022-07-26 19:14:49 +08:00
方块君
37dcd0f1a7 Update README 2022-07-22 23:03:25 +08:00
方块君
a4048d4fd1 Add auto sending player list 2022-07-22 22:08:02 +08:00
方块君
e62b1d4967 Change the server authentication method 2022-07-22 21:21:12 +08:00
方块君
8030ef8034 Prevent some exceptions 2022-07-22 14:44:22 +08:00
方块君
67f3eb180d Change return type 2022-07-21 21:17:24 +08:00
方块君
3bc9b0ab14 Fix client disconnect not clean player list 2022-07-21 20:34:56 +08:00
方块君
8b91e50d02 Update README 2022-07-21 20:30:26 +08:00
方块君
684ab421bd Add multi server support 2022-07-21 00:50:42 +08:00
12740c64e3 Update event to v1.2.2-dev 2022-07-10 20:58:10 +08:00
a1ef41f373 Update version to v1.2.4 2022-07-10 20:57:48 +08:00
1ec9e3f09a Update README.md 2022-07-10 12:34:52 +08:00
1a3b2e4904 Merge pull request #7 from realqhc/master
fix issue from grasscutter upgrade
2022-06-24 12:39:00 +08:00
957098a2cf Update version to v1.2.3 2022-06-24 12:37:25 +08:00
Qihan Cai
910842c460 fix issue from grasscutter upgrade 2022-06-24 14:20:30 +10:00
8b05c9aa10 Fixed an issue(#5) caused by renaming CommandResponseEvent to ReceiveCommandFeedbackEvent
Update version to v1.2.2
2022-06-20 20:11:18 +08:00
d5c29b61ff Update README_en-US.md 2022-05-23 20:03:33 +08:00
f719ba97ad Merge remote-tracking branch 'origin/master' 2022-05-23 20:00:57 +08:00
9a15414f3c Add README_en-US.md 2022-05-23 20:00:44 +08:00
95f688e970 Update README.md 2022-05-19 21:35:23 +08:00
d291d2a8d8 Update version to v1.2.1 2022-05-18 10:06:30 +08:00
ff04a5f107 Update version to v1.3.0
Support Listen Console Command Response
2022-05-18 10:02:39 +08:00
d9b09e48ce Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.gitignore
2022-05-18 09:19:17 +08:00
d190576c33 Update version to v1.2.0
Add config
Add run console command
2022-05-16 23:01:17 +08:00
8cdfcf3dc3 Add .gitignore 2022-05-13 09:38:40 +08:00
15 changed files with 830 additions and 162 deletions

51
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,51 @@
name: "Build"
on:
workflow_dispatch: ~
push:
paths:
- "**.java"
branches:
- "master"
- "LunarCode"
pull_request:
paths:
- "**.java"
types:
- opened
- synchronize
- reopened
jobs:
Build-Jar:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: '17'
- name: Cache gradle files
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
./.gradle/loom-cache
key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle', 'gradle.properties', '**/*.accesswidener') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Download latest grasscutter jar
run: wget https://nightly.link/Melledy/LunarCore/workflows/build/development/LunarCore.zip && mkdir lib && unzip LunarCore.zip -d lib
- name: Change permission
run: chmod +x gradlew
- name: Run Gradle
run: ./gradlew jar
- name: Upload build
uses: actions/upload-artifact@v3
with:
name: OpenCommand
path: '*.jar'

3
.gitignore vendored
View File

@@ -23,3 +23,6 @@
hs_err_pid*
/lib/
.gradle
.idea
/build

202
README.md
View File

@@ -1,76 +1,202 @@
# gc-opencommand-plugin
一个为第三方客户端开放GC命令执行接口的插件
中文 | [English](README_en-US.md)
一个为第三方客户端开放LC命令执行接口的插件
`1.7.0` 起可以通过 `|` 或者换行来分隔多条命令,例如:
```shell
/a 1 | /a 2
/a 3
```
调用 `ping` 响应数据将包含插件版本号。
## 服务端安装
# 服务端安装
1. 在 [Release](https://github.com/jie65535/gc-opencommand-plugin/releases) 下载 `jar`
2. 放入 `plugins` 文件夹即可
2. 放入 `LunarCore/plugins` 文件夹
3. 重启 `LunarCore` 即可生效
# 构建说明
1. 克隆仓库
2. 在目录下新建 `lib` 目录
3.`grasscutter-1.1.x-dev.jar` 放入 `lib` 目录
4. `gradle build`
## 玩家使用流程
# 玩家使用流程
1. 在客户端中填写服务地址,确认是否支持
2. 填写UID发送验证码
3. 将游戏内收到的**4位整数验证码**填入客户端校验
1. 在远程工具中填写服务地址,查询插件状态
2. 填写UID发送验证码需要在线
3. 将游戏内收到的**4位整数验证码**填入工具校验
4. 享受便利!
# 客户端请求流程
## 控制台连接
1. 首次启动时,会在 `plugins` 目录下生成一个 `opencommand-plugin` 目录,打开并编辑 `config.json`
2. 设置 `consoleToken` 的值为你的连接秘钥建议使用至少32字符的长随机字符串。检测到为空时会自动生成生成时会在控制台中输出
3. 重新启动服务端即可生效配置
4. 在工具中选择控制台身份,并填写你的 `consoleToken` 即可以控制台身份运行指令
---
## 客户端请求流程
1. `ping` 确认是否支持 `opencommand` 插件
2. `sendCode` 向指定玩家发送验证码1分钟内不允许重发保存返回的 `token`
3. 使用 `token` 和**4位整数验证码**发送 `verify` 校验
4. 如果验证通过,可以使用该 `token` 执行 `command` 动作
## 插件构建说明
1. 克隆仓库
2. 在目录下新建 `lib` 目录
3.`LunarCore.jar` 放入 `lib` 目录
4. 执行 `gradle build`
---
# API `/opencommand/api`
## `config.json`
```json5
{
// 控制台连接令牌(检测到空时会自动生成)
"consoleToken": "",
// 验证码过期时间(秒)
"codeExpirationTime_S": 60,
// 临时令牌过期时间(秒)
"tempTokenExpirationTime_S": 300,
// 授权令牌最后使用过期时间(小时)
"tokenLastUseExpirationTime_H": 48,
}
```
## API `/opencommand/api`
示例
```
https://127.0.0.1/opencommand/api
```
# Request 请求
### Request 请求
```java
public final class JsonRequest {
public String token = "";
public String action = "";
public String server = "";
public Object data = null;
}
```
# Response 响应
### Response 响应
```java
public final class JsonResponse {
public int retcode = 200;
public String message = "success";
public String message = "Success";
public Object data;
}
```
# Actions 动作
## `ping`
data = null
### Actions 动作
## `sendCode`
### Request
data = uid (int)
### Response
data = token (string)
#### `测试连接`
## `verify` 要求 `token`
### Request
data = code (int)
### Response
#### Success:
code = 200
#### Verification failed:
code = 400
##### Request
## `command` 要求 `token`
### Request
data = command (string)
### Response
data = message (string)
| 请求参数 | 请求数据 | 类型 |
|--------|--------|----------|
| action | `ping` | `String` |
##### Response
| 返回参数 | 返回数据 | 类型 |
|---------|-----------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `null` | `null` |
#### `发送验证码`
##### Request
| 请求参数 | 请求数据 | 类型 |
|--------|------------|----------|
| action | `sendCode` | `String` |
| data | `uid` | `Int` |
##### Response
| 返回参数 | 返回数据 | 类型 |
|---------|-----------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `token` | `String` |
#### `验证验证码`
##### Request
| 请求参数 | 请求数据 | 类型 |
|--------|----------|----------|
| action | `verify` | `String` |
| token | `token` | `String` |
| data | `code` | `Int` |
##### Response
成功
| 返回参数 | 返回数据 | 类型 |
|---------|-----------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `null` | `null` |
失败
| 返回参数 | 返回数据 | 类型 |
|---------|-----------------------|----------|
| retcode | `400` | `Int` |
| message | `Verification failed` | `String` |
| data | `null` | `null` |
#### `执行命令`
##### Request
| 请求参数 | 请求数据 | 类型 |
|--------|-----------|----------|
| action | `command` | `String` |
| token | `token` | `String` |
| data | `command` | `String` |
##### Response
成功
| 返回参数 | 返回数据 | 类型 |
|---------|------------------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `Command return` | `String` |
### 执行控制台命令
#### `执行命令`
##### Request
> 如果为单服务器则无需填写服务器 UUID
| 请求参数 | 请求数据 | 类型 |
|--------|-----------|----------|
| action | `command` | `String` |
| token | `token` | `String` |
| server | `UUID` | `String` |
| data | `command` | `String` |
##### Response
成功
| 返回参数 | 返回数据 | 类型 |
|---------|------------------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `Command return` | `String` |

207
README_en-US.md Normal file
View File

@@ -0,0 +1,207 @@
# gc-opencommand-plugin
[中文](README.md) | English
A plugin that opens the LC command execution interface for third-party clients
multiple commands can be separated by `|` or newline, for example:
```shell
/a 1 | /a 2
/a 3
```
Invoking `ping` the response data will contain the plugin version.
## Server installation
1. Download the `jar` in [Release](https://github.com/jie65535/gc-opencommand-plugin/releases)
2. Put it in the `LunarCore/plugins` folder
3. Restart `LunarCore` server
## Player
1. Fill in the service address in the Tool to check plugin status
2. Fill in the UID and send the verification code
3. Fill in the **4-digit integer verification code** received in the game into the Tool verification
4. Enjoy the convenience!
## Console connection
1. When starting for the first time, a `opencommand-plugin` directory will be generated under the `plugins` directory,
open and edit `config.json`
2. Set the value of `consoleToken` to your connection key. It is recommended to use a long random string of at least 32
characters. (automatically generated when empty is detected)
3. Restart the server to take effect
4. Select the console identity in the client, and fill in your `consoleToken` to run the command as the console identity
---
## Client request
1. `ping` to confirm whether the `opencommand` plugin is supported
2. `sendCode` sends a verification code to the specified player (re-send is not allowed within 1 minute), and save the
returned `token`
3. Send `verify` check using `token` and **4-digit integer verification code**
4. If the verification is passed, you can use the `token` to execute the `command` action
## Build
1. `git clone https://github.com/jie65535/gc-opencommand-plugin`
2. `cd gc-opencommand-plugin`
3. `mkdir lib`
4. `mv path/to/LunarCore.jar ./lib`
5. `gradle build`
---
## `config.json`
```json5
{
// console connection token (automatically generated when empty is detected)
"consoleToken": "",
// Verification code expiration time (seconds)
"codeExpirationTime_S": 60,
// Temporary token expiration time (seconds)
"tempTokenExpirationTime_S": 300,
// Authorization token last used expiration time (hours)
"tokenLastUseExpirationTime_H": 48,
}
```
## API `/opencommand/api`
Example
```
https://127.0.0.1/opencommand/api
```
## Request
```java
public final class JsonRequest {
public String token = "";
public String action = "";
public Seting server = "";
public Object data = null;
}
```
## Response
```java
public final class JsonResponse {
public int retcode = 200;
public String message = "Success";
public Object data;
}
```
### Actions
#### `Test connect`
##### Request
| Request | Request data | type |
|---------|--------------|----------|
| action | `ping` | `String` |
##### Response
| Response | Response data | type |
|----------|---------------|----------|
| retcode | `200` | `String` |
| message | `Success` | `String` |
| data | `null` | `null` |
#### `Send code`
##### Request
| Request | Request data | type |
|---------|--------------|----------|
| action | `sendCode` | `String` |
| data | `uid` | `Int` |
##### Response
| Response | Response data | type |
|----------|---------------|----------|
| retcode | `200` | `String` |
| message | `Success` | `String` |
| data | `token` | `String` |
#### `Verify code`
##### Request
| Request | Request data | type |
|---------|--------------|----------|
| action | `verify` | `String` |
| token | `token` | `String` |
| data | `code` | `Int` |
##### Response
Success
| Response | Response data | type |
|----------|---------------|----------|
| retcode | `200` | `String` |
| message | `Success` | `String` |
| data | `null` | `null` |
Failed
| Response | Response data | type |
|----------|-----------------------|----------|
| retcode | `400` | `String` |
| message | `Verification failed` | `String` |
| data | `null` | `null` |
#### `Run command`
##### Request
| Request | Request data | type |
|---------|--------------|----------|
| action | `command` | `String` |
| token | `token` | `String` |
| data | `command` | `String` |
##### Response
Success
| Response | Response data | type |
|----------|------------------|----------|
| retcode | `200` | `String` |
| message | `Success` | `String` |
| data | `Command return` | `String` |
### Run console command
#### `Run command`
##### Request
> If it is a single server, there is no need to fill in server UUID.
| Request | Request data | Type |
|---------|--------------|----------|
| action | `command` | `String` |
| token | `token` | `String` |
| server | `UUID` | `String` |
| data | `command` | `String` |
##### Response
Success
| Request | Response data | Type |
|---------|------------------|----------|
| retcode | `200` | `Int` |
| message | `Success` | `String` |
| data | `Command return` | `String` |

View File

@@ -4,7 +4,7 @@ plugins {
}
group 'com.github.jie65535.opencommand'
version 'dev-1.1.0'
version '1.0.0'
sourceCompatibility = 17
targetCompatibility = 17
@@ -18,7 +18,7 @@ dependencies {
}
jar {
jar.baseName = 'opencommand'
jar.baseName = 'OpenCommand-LunarCore'
destinationDir = file(".")
}

View File

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

View File

@@ -0,0 +1,63 @@
/*
* gc-opencommand
* Copyright (C) 2022-2023 jie65535
*
* 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 com.github.jie65535.opencommand;
public final class EventListeners {
// private static StringBuilder consoleMessageHandler;
// private static final Int2ObjectMap<StringBuilder> playerMessageHandlers = new Int2ObjectOpenHashMap<>();
//
// public static void setConsoleMessageHandler(StringBuilder handler) {
// consoleMessageHandler = handler;
// }
//
// /**
// * 获取新的玩家消息处理类
// * 获取时将创建或清空消息处理器并返回实例,**在执行命令前获取!**
// * @param uid 玩家uid
// * @return 新的玩家消息处理类
// */
// public static StringBuilder getPlayerMessageHandler(int uid) {
// var handler = playerMessageHandlers.get(uid);
// if (handler == null) {
// handler = new StringBuilder();
// playerMessageHandlers.put(uid, handler);
// }
// return handler;
// }
//
// /**
// * 命令执行反馈事件处理
// */
// public static void onCommandResponse(ReceiveCommandFeedbackEvent event) {
// StringBuilder handler;
// if (event.getPlayer() == null) {
// handler = consoleMessageHandler;
// } else {
// handler = playerMessageHandlers.get(event.getPlayer().getUid());
// }
//
// if (handler != null) {
// if (!handler.isEmpty()) {
// // New line
// handler.append(System.lineSeparator());
// }
// handler.append(event.getMessage());
// }
// }
}

View File

@@ -0,0 +1,40 @@
/*
* gc-opencommand
* Copyright (C) 2022-2023 jie65535
*
* 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 com.github.jie65535.opencommand;
public class OpenCommandConfig {
/**
* 控制台 Token
*/
public String consoleToken = "";
/**
* 验证码过期时间(单位秒)
*/
public int codeExpirationTime_S = 60;
/**
* 临时Token过期时间单位秒
*/
public int tempTokenExpirationTime_S = 300;
/**
* Token 最后使用过期时间(单位小时)
*/
public int tokenLastUseExpirationTime_H = 48;
}

View File

@@ -0,0 +1,42 @@
package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.model.Client;
import java.util.Date;
import java.util.Vector;
/**
* 插件持久化数据
*/
public class OpenCommandData {
/**
* 连接的客户端列表
*/
public Vector<Client> clients = new Vector<>();
/**
* 通过令牌获取客户端
* @param token 令牌
* @return 客户端对象若未找到返回null
*/
public Client getClientByToken(String token) {
for (var c : clients) {
if (c.token.equals(token))
return c;
}
return null;
}
/**
* 移除所有过期的客户端
*/
public void removeExpiredClients() {
var now = new Date();
clients.removeIf(client -> client.tokenExpireTime.before(now));
}
public void addClient(Client client) {
clients.add(client);
}
}

View File

@@ -1,6 +1,6 @@
/*
* gc-opencommand
* Copyright (C) 2022 jie65535
* Copyright (C) 2022-2023 jie65535
*
* 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
@@ -19,136 +19,156 @@ package com.github.jie65535.opencommand;
import com.github.jie65535.opencommand.json.JsonRequest;
import com.github.jie65535.opencommand.json.JsonResponse;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.server.http.Router;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.MessageHandler;
import emu.grasscutter.utils.Utils;
import express.Express;
import express.http.Request;
import express.http.Response;
import io.javalin.Javalin;
import com.github.jie65535.opencommand.model.Client;
import emu.lunarcore.LunarCore;
import emu.lunarcore.game.player.Player;
import emu.lunarcore.util.Crypto;
import io.javalin.http.Context;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public final class OpenCommandHandler implements Router {
public final class OpenCommandHandler {
@Override
public void applyRoutes(Express express, Javalin javalin) {
express.post("/opencommand/api", OpenCommandHandler::handle);
}
private static final Map<String, Integer> clients = new HashMap<>();
private static final Map<String, Date> tokenExpireTime = new HashMap<>();
private static final Map<String, Integer> codes = new HashMap<>();
private static final Int2ObjectMap<Date> codeExpireTime = new Int2ObjectOpenHashMap<>();
public static void handle(Request request, Response response) {
// Trigger cleanup action
cleanupExpiredData();
var now = new Date();
private static final SecureRandom secureRandom = new SecureRandom();
var req = request.body(JsonRequest.class);
response.type("application/json");
if (req.action.equals("sendCode")) {
int playerId = (int) req.data;
var player = Grasscutter.getGameServer().getPlayerByUid(playerId);
if (player == null) {
response.json(new JsonResponse(404, "Player Not Found."));
} else {
if (codeExpireTime.containsKey(playerId)) {
var expireTime = codeExpireTime.get(playerId);
if (now.before(expireTime)) {
response.json(new JsonResponse(403, "Requests are too frequent"));
return;
}
}
public static void handle(Context context) {
var plugin = OpenCommandPlugin.getInstance();
try {
var config = plugin.getConfig();
var data = plugin.getData();
var now = new Date();
// Trigger cleanup action
cleanupExpiredCodes();
data.removeExpiredClients();
String token = req.token;
if (token == null || token.isEmpty())
token = Utils.bytesToHex(Crypto.createSessionKey(32));
int code = Utils.randomRange(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + 60 * 1000));
tokenExpireTime.put(token, new Date(now.getTime() + 5 * 60 * 1000));
codes.put(token, code);
clients.put(token, playerId);
player.dropMessage("[Open Command] Verification code: {code}".replace("{code}", Integer.toString(code)));
response.json(new JsonResponse(token));
return;
}
} else if (req.action.equals("ping")) {
response.json(new JsonResponse());
return;
}
// token is required
if (!clients.containsKey(req.token)) {
response.json(new JsonResponse(401, "Unauthorized"));
return;
}
if (codes.containsKey(req.token)) {
if (req.action.equals("verify")) {
if (codes.get(req.token).equals(req.data)) {
codes.remove(req.token);
// update token expire time
tokenExpireTime.put(req.token, new Date(now.getTime() + 60 * 60 * 1000));
response.json(new JsonResponse());
} else {
response.json(new JsonResponse(400, "Verification failed"));
}
return;
}
} else {
if (req.action.equals("command")) {
// update token expire time
tokenExpireTime.put(req.token, new Date(now.getTime() + 4 * 60 * 60 * 1000));
var playerId = clients.get(req.token);
var player = Grasscutter.getGameServer().getPlayerByUid(playerId);
var command = req.data.toString();
var req = context.bodyAsClass(JsonRequest.class);
if (req.action.equals("sendCode")) {
int playerId = (int)Double.parseDouble(req.data.toString());
var player = LunarCore.getGameServer().getPlayerByUid(playerId, false);
if (player == null) {
response.json(new JsonResponse(404, "Player not found"));
context.json(new JsonResponse(404, "Player Not Found."));
} else {
if (codeExpireTime.containsKey(playerId)) {
var expireTime = codeExpireTime.get(playerId);
if (now.before(expireTime)) {
context.json(new JsonResponse(403, "Requests are too frequent"));
return;
}
}
String token = req.token;
if (token == null || token.isEmpty())
token = Crypto.createSessionKey(player.getAccountUid());
int code = secureRandom.nextInt(1000, 9999);
codeExpireTime.put(playerId, new Date(now.getTime() + config.codeExpirationTime_S * 1000L));
codes.put(token, code);
data.addClient(new Client(token, playerId, new Date(now.getTime() + config.tempTokenExpirationTime_S * 1000L)));
player.sendMessage("[Open Command] Verification code: " + code);
context.json(new JsonResponse(token));
}
return;
} else if (req.action.equals("ping")) {
context.json(new JsonResponse(plugin.getVersion()));
return;
}
// token is required
if (req.token == null || req.token.isEmpty()) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
var isConsole = req.token.equals(config.consoleToken);
var client = data.getClientByToken(req.token);
if (!isConsole && client == null) {
context.json(new JsonResponse(401, "Unauthorized"));
return;
}
if (isConsole) {
if (req.action.equals("verify")) {
context.json(new JsonResponse());
return;
} else if (req.action.equals("command")) {
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (plugin) {
try {
plugin.getLogger().info(String.format("IP: %s run command in console > %s", context.ip(), req.data));
tryInvokeCommand(null, req.data.toString());
context.json(new JsonResponse("OK"));
} catch (Exception e) {
plugin.getLogger().warn("Run command failed.", e);
context.json(new JsonResponse(500, "error", e.getLocalizedMessage()));
}
}
return;
} else if (req.action.equals("runmode")) {
context.json(new JsonResponse(200, "Success", 0));
return;
}
// Player MessageHandler do not support concurrency
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (player) {
try {
var resultCollector = new MessageHandler();
player.setMessageHandler(resultCollector);
CommandMap.getInstance().invoke(player, player, command);
response.json(new JsonResponse(resultCollector.getMessage()));
} catch (Exception e) {
response.json(new JsonResponse(500, "error", e.getLocalizedMessage()));
} finally {
player.setMessageHandler(null);
} else if (codes.containsKey(req.token)) {
if (req.action.equals("verify")) {
if (codes.get(req.token) == (int)Double.parseDouble(req.data.toString())) {
codes.remove(req.token);
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
context.json(new JsonResponse());
plugin.getLogger().info(String.format("Player %d has passed the verification, ip: %s", client.playerId, context.ip()));
plugin.saveData();
} else {
context.json(new JsonResponse(400, "Verification failed"));
}
return;
}
} else {
if (req.action.equals("command")) {
// update token expire time
client.tokenExpireTime = new Date(now.getTime() + config.tokenLastUseExpirationTime_H * 60L * 60L * 1000L);
var player = LunarCore.getGameServer().getPlayerByUid(client.playerId, false);
if (player == null) {
context.json(new JsonResponse(404, "Player not found"));
return;
}
// var history = player.getChatManager().getHistoryByUid(GameConstants.SERVER_CONSOLE_UID);
try {
tryInvokeCommand(player, req.data.toString());
context.json(new JsonResponse("OK"));
} catch (Exception e) {
plugin.getLogger().warn("Run command failed.", e);
context.json(new JsonResponse(500, "error", e.getLocalizedMessage()));
}
return;
}
return;
}
context.json(new JsonResponse(403, "forbidden"));
} catch (Throwable ex) {
plugin.getLogger().error("[OpenCommand] handler error.", ex);
}
response.json(new JsonResponse(403, "forbidden"));
}
private static void cleanupExpiredData() {
private static void tryInvokeCommand(Player sender, String rawMessage) {
for (var command : rawMessage.split("\n[/!]|\\|")) {
command = command.trim();
if (command.isEmpty()) continue;
if (command.charAt(0) == '/' || command.charAt(0) == '!') {
command = command.substring(1);
}
LunarCore.getCommandManager().invoke(sender, command);
}
}
private static void cleanupExpiredCodes() {
var now = new Date();
codeExpireTime.int2ObjectEntrySet().removeIf(entry -> entry.getValue().before(now));
var it = tokenExpireTime.entrySet().iterator();
while (it.hasNext()) {
var entry = it.next();
if (entry.getValue().before(now)) {
it.remove();
// remove expired token
clients.remove(entry.getKey());
}
}
if (codeExpireTime.isEmpty())
codes.clear();
}
}

View File

@@ -1,6 +1,6 @@
/*
* gc-opencommand
* Copyright (C) 2022 jie65535
* Copyright (C) 2022-2023 jie65535
*
* 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
@@ -17,23 +17,121 @@
*/
package com.github.jie65535.opencommand;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.plugin.Plugin;
import emu.lunarcore.LunarCore;
import emu.lunarcore.plugin.Plugin;
import emu.lunarcore.util.Crypto;
import emu.lunarcore.util.JsonUtils;
import org.slf4j.Logger;
import java.io.*;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public final class OpenCommandPlugin extends Plugin {
private static OpenCommandPlugin instance;
public OpenCommandPlugin(Identifier identifier, URLClassLoader classLoader, File dataFolder, Logger logger) {
super(identifier, classLoader, dataFolder, logger);
}
public static OpenCommandPlugin getInstance() {
return instance;
}
private OpenCommandConfig config;
private OpenCommandData data;
public class OpenCommandPlugin extends Plugin {
@Override
public void onLoad() {
instance = this;
// 加载配置
loadConfig();
// 加载数据
loadData();
}
@Override
public void onEnable() {
Grasscutter.getHttpServer().addRouter(OpenCommandHandler.class);
Grasscutter.getLogger().info("[OpenCommand] Enabled");
LunarCore.getHttpServer().getApp().post("/opencommand/api", OpenCommandHandler::handle);
getLogger().info("[OpenCommand] Enabled. https://github.com/jie65535/gc-opencommand-plugin");
}
@Override
public void onDisable() {
Grasscutter.getLogger().info("[OpenCommand] Disabled");
saveData();
getLogger().info("[OpenCommand] Disabled");
}
public OpenCommandConfig getConfig() {
return config;
}
public OpenCommandData getData() {
return data;
}
private void loadConfig() {
var configFile = new File(getDataFolder(), "config.json");
if (!configFile.exists()) {
config = new OpenCommandConfig();
saveConfig();
} else {
try {
config = JsonUtils.decode(Files.readString(configFile.toPath(), StandardCharsets.UTF_8),
OpenCommandConfig.class);
} catch (Exception exception) {
config = new OpenCommandConfig();
getLogger().error("[OpenCommand] There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json.");
}
}
// 检查控制台Token
if (config.consoleToken == null || config.consoleToken.isEmpty()) {
config.consoleToken = Crypto.createSessionKey("1");
saveConfig();
getLogger().warn("Detected that consoleToken is empty, automatically generated Token for you as follows: {}", config.consoleToken);
}
}
private void saveConfig() {
var configFile = new File(getDataFolder(), "config.json");
try (var file = new FileWriter(configFile)) {
file.write(JsonUtils.encode(config));
} catch (IOException e) {
getLogger().error("[OpenCommand] Unable to write to config file.");
} catch (Exception e) {
getLogger().error("[OpenCommand] Unable to save config file.");
}
}
private void loadData() {
var dataFile = new File(getDataFolder(), "data.json");
if (!dataFile.exists()) {
data = new OpenCommandData();
saveData();
} else {
try {
data = JsonUtils.decode(Files.readString(dataFile.toPath(), StandardCharsets.UTF_8),
OpenCommandData.class);
} catch (Exception exception) {
getLogger().error("[OpenCommand] There was an error while trying to load the data from data.json. Please make sure that there are no syntax errors. If you want to start with a default data, delete your existing data.json.");
}
if (data == null) {
data = new OpenCommandData();
}
}
}
public void saveData() {
try (var file = new FileWriter(new File(getDataFolder(), "data.json"))) {
file.write(JsonUtils.encode(data));
} catch (IOException e) {
getLogger().error("[OpenCommand] Unable to write to data file.");
} catch (Exception e) {
getLogger().error("[OpenCommand] Unable to save data file.");
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* gc-opencommand
* Copyright (C) 2022 jie65535
* Copyright (C) 2022-2023 jie65535
*
* 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
@@ -20,5 +20,6 @@ package com.github.jie65535.opencommand.json;
public final class JsonRequest {
public String token = "";
public String action = "";
public String server = "";
public Object data = null;
}

View File

@@ -1,6 +1,6 @@
/*
* gc-opencommand
* Copyright (C) 2022 jie65535
* Copyright (C) 2022-2023 jie65535
*
* 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
@@ -19,7 +19,7 @@ package com.github.jie65535.opencommand.json;
public final class JsonResponse {
public int retcode = 200;
public String message = "success";
public String message = "Success";
public Object data;
public JsonResponse() {

View File

@@ -0,0 +1,16 @@
package com.github.jie65535.opencommand.model;
import java.util.Date;
public final class Client {
public String token;
public Integer playerId;
public Date tokenExpireTime;
public Client(String token, Integer playerId, Date tokenExpireTime) {
this.token = token;
this.playerId = playerId;
this.tokenExpireTime = tokenExpireTime;
}
}

View File

@@ -1,7 +1,8 @@
{
"name": "opencommand-plugin",
"description": "Open command interface for third-party clients",
"version": "dev-1.1.0",
"version": "1.0.0",
"mainClass": "com.github.jie65535.opencommand.OpenCommandPlugin",
"authors": ["jie65535"]
"authors": ["jie65535"],
"api": 1
}