添加 S3支持 完成皮肤上传和安全验证
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
e143d3aa6c
commit
32f7577a07
|
@ -3,5 +3,6 @@ node_modules
|
||||||
production
|
production
|
||||||
**/*.key
|
**/*.key
|
||||||
**/*.pem
|
**/*.pem
|
||||||
# My own launch script which should NEVER upload since there is secrets!
|
|
||||||
|
# You should NEVER EVER UPLOAD THIS FILE
|
||||||
launch.ps1
|
launch.ps1
|
28
README.MD
28
README.MD
|
@ -1,4 +1,4 @@
|
||||||
<div aligen='center'>
|
<div align='center'>
|
||||||
<img src='logo.png'/>
|
<img src='logo.png'/>
|
||||||
<h1>LSP Yggdrasil</h1>
|
<h1>LSP Yggdrasil</h1>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
老色批世界树 —— 一个高性能麻将、奥苏力不-印寨克托接口的实现。使用fastify来把处理速度加速到老色批的速度(
|
老色批世界树 —— 一个高性能麻将、奥苏力不-印寨克托接口的实现。使用fastify来把处理速度加速到老色批的速度(
|
||||||
|
|
||||||
具体有多快呢?登录处理从数据包发出到接收到服务端响应仅需要 __***6ms***__(根据机器不同可能会有浮动,以实际情况为准)!
|
具体有多快呢?登录处理从数据包发出到接收到服务端响应仅需要 __***5ms***__(根据机器不同可能会有浮动,以实际情况为准)!
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -23,18 +23,14 @@
|
||||||
+ [x] /sessionserver
|
+ [x] /sessionserver
|
||||||
+ [x] 测试
|
+ [x] 测试
|
||||||
+ [x] /api
|
+ [x] /api
|
||||||
- [ ] 进阶 API
|
- [x] 进阶 API
|
||||||
- [ ] 皮肤上传和安全检查
|
- [x] 皮肤上传和安全检查
|
||||||
+ [x] 皮肤数据的RSA签名
|
+ [x] 皮肤数据的RSA签名
|
||||||
+ [ ] 皮肤上传
|
+ [x] 皮肤上传
|
||||||
+ [x] 安全检查
|
+ [x] 安全检查
|
||||||
- [ ] 兼容S3后端
|
- [x] 兼容S3后端
|
||||||
- [x] 服务器状态接口
|
- [x] 服务器状态接口
|
||||||
- [x] authlib-injector 元数据接口
|
- [x] authlib-injector 元数据接口
|
||||||
- [ ] TGbot前端
|
|
||||||
+ [x] 注册
|
|
||||||
+ [ ] 用户查询、改密等
|
|
||||||
+ [ ] 管理员指令
|
|
||||||
|
|
||||||
#### Release 1.0
|
#### Release 1.0
|
||||||
- [ ] 单元测试
|
- [ ] 单元测试
|
||||||
|
@ -44,11 +40,7 @@
|
||||||
- [ ] /api
|
- [ ] /api
|
||||||
- [ ] Advanced API
|
- [ ] Advanced API
|
||||||
+ [ ] Utils
|
+ [ ] Utils
|
||||||
|
|
||||||
#### 未来的版本:
|
|
||||||
|
|
||||||
- [ ] 完整 web 管理
|
- [ ] 完整 web 管理
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
@ -62,6 +54,11 @@
|
||||||
4. 使用 `$ node build.js` 创建运行时构建
|
4. 使用 `$ node build.js` 创建运行时构建
|
||||||
5. 使用 `$ node path/to/lsp-yggdrasil.full.cjs` 起飞
|
5. 使用 `$ node path/to/lsp-yggdrasil.full.cjs` 起飞
|
||||||
|
|
||||||
|
###### 若需要开发者环境您还需要额外几步:
|
||||||
|
|
||||||
|
1. 执行 `$ yarn dev:mklaunch` 来生成启动脚本
|
||||||
|
2. 使用 `$ yarn dev` 启动开发模式服务器
|
||||||
|
|
||||||
> 注:对于开发者的首次运行,您可能需要创建一个测试账户,添加环境参数:`DEVEL_FIRST_RUN=1` 即可创建一个测试玩家
|
> 注:对于开发者的首次运行,您可能需要创建一个测试账户,添加环境参数:`DEVEL_FIRST_RUN=1` 即可创建一个测试玩家
|
||||||
> 测试玩家的账户为: `i@lama.icu` 密码为 `123456` 您可以到 `index.js` 中自由修改
|
> 测试玩家的账户为: `i@lama.icu` 密码为 `123456` 您可以到 `index.js` 中自由修改
|
||||||
|
|
||||||
|
@ -70,6 +67,9 @@
|
||||||
- Q:支持 `https` 嘛?
|
- Q:支持 `https` 嘛?
|
||||||
- A:不支持,请使用反代来使用`https`,或者您也可以修改 `index.js` 中初始化代码。
|
- A:不支持,请使用反代来使用`https`,或者您也可以修改 `index.js` 中初始化代码。
|
||||||
|
|
||||||
|
- Q: 支持 `IPv6` 嘛?
|
||||||
|
- A: 完美支持 `IPv6`
|
||||||
|
|
||||||
## 内置的 yarn 指令
|
## 内置的 yarn 指令
|
||||||
|
|
||||||
+ `dev` —— 启动开发环境服务器
|
+ `dev` —— 启动开发环境服务器
|
||||||
|
|
12
package.json
12
package.json
|
@ -13,18 +13,20 @@
|
||||||
"shelljs.exec": "^1.1.8"
|
"shelljs.exec": "^1.1.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/swagger": "^6.0.1",
|
"@aws-sdk/client-s3": "^3.121.0",
|
||||||
"aws-sdk": "^2.1140.0",
|
"@aws-sdk/s3-request-presigner": "^3.121.0",
|
||||||
|
"@fastify/swagger": "^7.4.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"fastify": "^3.29.0",
|
"fastify": "^4.2.0",
|
||||||
"hex-to-uuid": "^1.1.1",
|
"hex-to-uuid": "^1.1.1",
|
||||||
"mongoose": "^6.3.1",
|
"mongoose": "^6.3.1",
|
||||||
|
"pino": "^8.1.0",
|
||||||
"pino-pretty": "^7.6.1",
|
"pino-pretty": "^7.6.1",
|
||||||
"pngjs": "^6.0.0",
|
"pngjs": "^6.0.0",
|
||||||
"telegraf": "^4.8.2"
|
"telegraf": "^4.8.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --watch src/index.js",
|
"dev": "node launch-development.js",
|
||||||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules UNIT_TEST=1 NODE_NO_WARNINGS=1 jest"
|
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules UNIT_TEST=1 NODE_NO_WARNINGS=1 jest",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,12 @@ export const config = {
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
bucket: "",
|
bucket: "",
|
||||||
key: "",
|
key: "",
|
||||||
|
secret: "",
|
||||||
|
extra: { // these configs will pass to s3client directly
|
||||||
|
region: 'us-ashburn-02',
|
||||||
|
forcePathStyle: true,
|
||||||
|
signatureVersion: 'v4',
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
signing: { // 签名材质信息使用
|
signing: { // 签名材质信息使用
|
||||||
|
|
13
src/hooks.js
13
src/hooks.js
|
@ -5,12 +5,8 @@ export async function headerValidation(req, rep) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(Object.keys(req.headers).some(key => {
|
if(Object.keys(req.headers).some(key => {
|
||||||
req.log.info(key.toLowerCase() === "content-type")
|
|
||||||
req.log.info(req.headers[key].toLowerCase())
|
|
||||||
return key.toLowerCase() === "content-type" && req.headers[key].toLowerCase().indexOf("application/json") === -1
|
return key.toLowerCase() === "content-type" && req.headers[key].toLowerCase().indexOf("application/json") === -1
|
||||||
})) {
|
})) {
|
||||||
req.log.info(JSON.stringify(req.headers))
|
|
||||||
|
|
||||||
return rep.code(400).send({
|
return rep.code(400).send({
|
||||||
error: "IllegalArgumentException",
|
error: "IllegalArgumentException",
|
||||||
errorMessage: "请求内容不正确",
|
errorMessage: "请求内容不正确",
|
||||||
|
@ -20,9 +16,18 @@ export async function headerValidation(req, rep) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleError(err, req, rep) {
|
export async function handleError(err, req, rep) {
|
||||||
|
req.log.info(err)
|
||||||
|
if(!(/(authserver)|(sessionserver)|(api)/g).test(req.url)) {
|
||||||
return rep.code(500).send({
|
return rep.code(500).send({
|
||||||
error: "InternalServerError",
|
error: "InternalServerError",
|
||||||
errorMessage: "服务器内部错误",
|
errorMessage: "服务器内部错误",
|
||||||
cause: err.message
|
cause: err.message
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
return await rep.code(500).send({
|
||||||
|
err: 0.000000,
|
||||||
|
msg: "服务器内部错误",
|
||||||
|
extra: err
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
56
src/index.js
56
src/index.js
|
@ -12,7 +12,9 @@ import { Scenes, session, Telegraf } from 'telegraf'
|
||||||
import { allScenes, registerAllPlayerCommands } from './telegram/player-commands.js'
|
import { allScenes, registerAllPlayerCommands } from './telegram/player-commands.js'
|
||||||
import { Player } from './models/player.js'
|
import { Player } from './models/player.js'
|
||||||
import fastifySwagger from '@fastify/swagger'
|
import fastifySwagger from '@fastify/swagger'
|
||||||
import S3 from 'aws-sdk/clients/s3.js'
|
import { S3Client } from '@aws-sdk/client-s3'
|
||||||
|
import multipart from '@fastify/multipart'
|
||||||
|
import pino from 'pino'
|
||||||
|
|
||||||
String.prototype._split = String.prototype.split
|
String.prototype._split = String.prototype.split
|
||||||
|
|
||||||
|
@ -43,31 +45,43 @@ for(let i = 0; i < process.argv.length; i++) {
|
||||||
const curr = process.argv[i]
|
const curr = process.argv[i]
|
||||||
if(curr.startsWith('--')) {
|
if(curr.startsWith('--')) {
|
||||||
switch(curr.substring(2)){
|
switch(curr.substring(2)){
|
||||||
case 'override':
|
case 'override': {
|
||||||
const [next, value] = process.argv[i + 1].split(":", 2)
|
const [next, value] = process.argv[i + 1].split(":", 2)
|
||||||
if(!next || next.startsWith('--')) {
|
if(next || !next.startsWith('--')) {
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
eval(`config.${next} = '${value}'`)
|
eval(`config.${next} = '${value}'`)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const server = fastify({
|
export const serverLogger = pino({
|
||||||
logger: {
|
transport: {
|
||||||
prettyPrint: true,
|
target: 'pino-pretty'
|
||||||
// level: 'error'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const server = fastify({
|
||||||
|
logger: serverLogger
|
||||||
|
})
|
||||||
|
|
||||||
export const telegraf = new Telegraf(config.telegram.token)
|
export const telegraf = new Telegraf(config.telegram.token)
|
||||||
|
|
||||||
export const s3Instance = new S3({
|
export const s3Instance = new S3Client({
|
||||||
|
credentials: {
|
||||||
accessKeyId: config.storage.key,
|
accessKeyId: config.storage.key,
|
||||||
|
secretAccessKey: config.storage.secret,
|
||||||
|
},
|
||||||
endpoint: config.storage.endpoint,
|
endpoint: config.storage.endpoint,
|
||||||
|
...config.storage.extra
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
export const setup = async () => {
|
export const setup = async () => {
|
||||||
server.log.info("老色批世界树 > 初始化中...")
|
server.log.info("老色批世界树 > 初始化中...")
|
||||||
|
|
||||||
|
@ -77,10 +91,20 @@ export const setup = async () => {
|
||||||
|
|
||||||
server.decorate('keys', { publicKey, privateKey })
|
server.decorate('keys', { publicKey, privateKey })
|
||||||
|
|
||||||
s3Instance.putObject()
|
|
||||||
|
|
||||||
config.custom.preHooks(server)
|
config.custom.preHooks(server)
|
||||||
|
|
||||||
server.addHook('preHandler', Hooks.headerValidation)
|
server.addHook('preHandler', Hooks.headerValidation)
|
||||||
|
server.setErrorHandler(Hooks.handleError)
|
||||||
|
|
||||||
|
server.addContentTypeParser('image/png', (_, payload, done) => {
|
||||||
|
done(null, payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
server.register(multipart, {
|
||||||
|
limit: {
|
||||||
|
fileSize: 5 * 1024 * 1024 // 5MB/40Mb
|
||||||
|
}
|
||||||
|
})
|
||||||
server.register(fastifySwagger, {
|
server.register(fastifySwagger, {
|
||||||
routePrefix: '/docs',
|
routePrefix: '/docs',
|
||||||
swagger: {
|
swagger: {
|
||||||
|
@ -116,6 +140,8 @@ export const setup = async () => {
|
||||||
|
|
||||||
server.route(WebAPIRoutings.login)
|
server.route(WebAPIRoutings.login)
|
||||||
server.route(WebAPIRoutings.register)
|
server.route(WebAPIRoutings.register)
|
||||||
|
server.route(WebAPIRoutings.textures)
|
||||||
|
server.route(WebAPIRoutings.uploadTexture)
|
||||||
|
|
||||||
config.custom.postRouting(server)
|
config.custom.postRouting(server)
|
||||||
|
|
||||||
|
@ -143,6 +169,8 @@ export const setup = async () => {
|
||||||
telegraf.use(session())
|
telegraf.use(session())
|
||||||
telegraf.use(stage.middleware())
|
telegraf.use(stage.middleware())
|
||||||
|
|
||||||
|
// server.log.info("老色批世界树 > 初始化中...")
|
||||||
|
|
||||||
registerAllPlayerCommands()
|
registerAllPlayerCommands()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,9 +178,9 @@ const launch = async () => {
|
||||||
process.on('SIGINT', shutdown)
|
process.on('SIGINT', shutdown)
|
||||||
process.on('SIGTERM', shutdown)
|
process.on('SIGTERM', shutdown)
|
||||||
|
|
||||||
telegraf.launch()
|
await telegraf.launch()
|
||||||
|
|
||||||
await server.listen(config.server.port, config.server.url)
|
await server.listen({ port: config.server.port, url: config.server.url })
|
||||||
|
|
||||||
server.log.info("老色批世界树 > 基于 fastify 的高性能 HTTP 服务器已启动")
|
server.log.info("老色批世界树 > 基于 fastify 的高性能 HTTP 服务器已启动")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import mongoose from 'mongoose'
|
import mongoose from 'mongoose'
|
||||||
import { uuidToNoSymboUUID } from '../generator.js'
|
import { uuidToNoSymboUUID } from '../generator.js'
|
||||||
import { ImageSecurity } from '../secure.js'
|
import { ImageSecurity } from '../secure.js'
|
||||||
import { server } from '../index.js'
|
import { s3Instance } from '../index.js'
|
||||||
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
||||||
|
import { GetObjectCommand } from '@aws-sdk/client-s3'
|
||||||
|
import { config } from '../config.js'
|
||||||
|
|
||||||
export const Player = mongoose.model("Player", new mongoose.Schema({
|
export const Player = mongoose.model("Player", new mongoose.Schema({
|
||||||
username: String, // 有符号 UUID
|
username: String, // 有符号 UUID
|
||||||
|
@ -72,7 +75,7 @@ export const PlayerAccountSerializationSchema = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPlayerSerialization(player) {
|
export async function getPlayerSerialization(player) {
|
||||||
const textures = {
|
const textures = {
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
profileId: uuidToNoSymboUUID(player.uuid),
|
profileId: uuidToNoSymboUUID(player.uuid),
|
||||||
|
@ -82,30 +85,23 @@ export function getPlayerSerialization(player) {
|
||||||
|
|
||||||
if(player.textures.skin && player.textures.skin != 0) { // Must be '!=' if this change to '!==' will never works
|
if(player.textures.skin && player.textures.skin != 0) { // Must be '!=' if this change to '!==' will never works
|
||||||
textures.textures.SKIN = {
|
textures.textures.SKIN = {
|
||||||
url: player.textures.skin
|
url: await getSignedUrl(s3Instance, new GetObjectCommand({
|
||||||
|
Bucket: config.storage.bucket,
|
||||||
|
Key: player.textures.skin
|
||||||
|
}), { expiresIn: 3 * 24 * 60 * 60 }) // 3 days
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(player.textures.cape && player.textures.cape != 0) { // Must be '!=' if this change to '!==' will never works
|
if(player.textures.cape && player.textures.cape != 0) { // Must be '!=' if this change to '!==' will never works
|
||||||
textures.textures.CAPE = {
|
textures.textures.CAPE = {
|
||||||
url: player.textures.cape,
|
url: await getSignedUrl(s3Instance, new GetObjectCommand({
|
||||||
|
Bucket: config.storage.bucket,
|
||||||
|
Key: player.textures.cape
|
||||||
|
}), { expiresIn: 3 * 24 * 60 * 60 }) // 3 days
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val = Buffer.from(JSON.stringify(textures)).toString('base64')
|
const val = Buffer.from(JSON.stringify(textures)).toString('base64')
|
||||||
|
|
||||||
server.log.info({
|
|
||||||
id: uuidToNoSymboUUID(player.uuid),
|
|
||||||
name: player.username,
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
name: "texturs",
|
|
||||||
value: val,
|
|
||||||
signature: ImageSecurity.sign(val),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: uuidToNoSymboUUID(player.uuid),
|
id: uuidToNoSymboUUID(player.uuid),
|
||||||
name: player.username,
|
name: player.username,
|
||||||
|
|
|
@ -107,7 +107,7 @@ export const authenticate = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = PlayerModel.getPlayerSerialization(player)
|
const profile = await PlayerModel.getPlayerSerialization(player)
|
||||||
|
|
||||||
await Token.deleteMany({ player: player.uuid }).exec()
|
await Token.deleteMany({ player: player.uuid }).exec()
|
||||||
|
|
||||||
|
@ -145,16 +145,22 @@ export const refresh = {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"clientToken": {
|
"clientToken": {
|
||||||
"type": "string",
|
anyOf: [
|
||||||
"optional": true
|
{ type: 'string' },
|
||||||
|
{ type: 'null' }
|
||||||
|
],
|
||||||
},
|
},
|
||||||
"requestUser": {
|
"requestUser": {
|
||||||
"type": "boolean",
|
anyOf: [
|
||||||
"optional": true
|
{ type: 'boolean' },
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"selectedProfile": {
|
"selectedProfile": {
|
||||||
"optional": true,
|
anyOf: [
|
||||||
...PlayerModel.PlayerSeriliazationSchema
|
{ ...PlayerModel.PlayerSeriliazationSchema },
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -247,7 +253,7 @@ export const refresh = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(selectedProfile) {
|
if(selectedProfile) {
|
||||||
response.selectedProfile = PlayerModel.getPlayerSerialization(player)
|
response.selectedProfile = await PlayerModel.getPlayerSerialization(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,8 +275,10 @@ export const validate = {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"clientToken": {
|
"clientToken": {
|
||||||
"type": "string",
|
anyOf: [
|
||||||
"optional": true
|
{ "type": "string" },
|
||||||
|
{ type: 'null' },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -329,8 +337,10 @@ export const invalidate = {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"clientToken": {
|
"clientToken": {
|
||||||
"type": "string",
|
anyOf: [
|
||||||
"optional": true
|
{ "type": "string" },
|
||||||
|
{ type: 'null' },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -379,8 +389,10 @@ export const signout = {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"password": {
|
"password": {
|
||||||
"type": "string",
|
anyOf: [
|
||||||
"optional": true
|
{ "type": "string" },
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -136,7 +136,7 @@ export const hasJoined = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await rep.code(200).send(getPlayerSerialization(player))
|
await rep.code(200).send(await getPlayerSerialization(player))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,8 +152,10 @@ export const profile = {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
unsigned: {
|
unsigned: {
|
||||||
"type": "boolean",
|
anyOf: [
|
||||||
"optional": true
|
{ "type": "boolean" },
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
response: {
|
response: {
|
||||||
|
@ -171,7 +173,7 @@ export const profile = {
|
||||||
return await rep.code(204).send()
|
return await rep.code(204).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
await rep.code(200).send(getPlayerSerialization(player))
|
await rep.code(200).send(await getPlayerSerialization(player))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
import { getOverrideHandler, getOverridePreHandler } from "../config.js"
|
import { config, getOverrideHandler, getOverridePreHandler } from "../config.js"
|
||||||
import { Player } from "../models/player.js"
|
import { Player } from "../models/player.js"
|
||||||
import { createHash } from "crypto"
|
import { createHash } from "crypto"
|
||||||
import { generateToken, uuid } from "../generator.js"
|
import { generateToken, uuid } from "../generator.js"
|
||||||
import { Token } from "../models/token.js"
|
import { Token } from "../models/token.js"
|
||||||
|
import { s3Instance } from "../index.js"
|
||||||
|
import { ImageSecurity } from "../secure.js"
|
||||||
|
import { PutObjectCommand } from "@aws-sdk/client-s3"
|
||||||
|
|
||||||
const BASE_RESPONSE = {
|
const BASE_RESPONSE = {
|
||||||
err: {
|
err: {
|
||||||
|
@ -20,12 +23,36 @@ const BASE_RESPONSE = {
|
||||||
const identifiers = new Map()
|
const identifiers = new Map()
|
||||||
|
|
||||||
async function identifierValidator(req, rep) {
|
async function identifierValidator(req, rep) {
|
||||||
const identifier = req.headers['X-LSP-Idenitifier']
|
const identifier = req.headers['x-lsp-identifier']
|
||||||
if(!identifier) {
|
if(!identifier) {
|
||||||
return await rep.code(401).send({
|
return await rep.code(400).send({
|
||||||
err: 1.143688,
|
err: 1.143688,
|
||||||
|
msg: "请求格式不正确"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!identifiers.has(identifier)) {
|
||||||
|
return await rep.code(401).send({
|
||||||
|
err: 0.456914,
|
||||||
|
msg: "用户不存在"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const {t, uuid} = identifiers.get(identifier)
|
||||||
|
if(t < Date.now()) {
|
||||||
|
return await rep.code(401).send({
|
||||||
|
err: 1.143688,
|
||||||
|
msg: "令牌超时"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
identifiers.set(identifier, {
|
||||||
|
t: Date.now() + 1000 * 60 * 60,
|
||||||
|
uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
req.player = await Player.findOne({ uuid })
|
||||||
|
req.t = t
|
||||||
}
|
}
|
||||||
|
|
||||||
export const login = {
|
export const login = {
|
||||||
|
@ -40,18 +67,15 @@ export const login = {
|
||||||
properties: {
|
properties: {
|
||||||
username: {
|
username: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '用户名',
|
description: '用户名'
|
||||||
example: 'test'
|
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '密码',
|
description: '密码'
|
||||||
example: '123456'
|
|
||||||
},
|
},
|
||||||
createToken: {
|
createToken: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: '是否创建一个 accessToken',
|
description: '是否创建一个 accessToken'
|
||||||
example: false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -63,52 +87,36 @@ export const login = {
|
||||||
extra: {
|
extra: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: '额外信息',
|
description: '额外信息',
|
||||||
example: {
|
|
||||||
identifier: '<token>',
|
|
||||||
textures: {
|
|
||||||
skin: '<url>',
|
|
||||||
cape: '<url>'
|
|
||||||
},
|
|
||||||
username: '<username>',
|
|
||||||
uuid: '<uuid>'
|
|
||||||
},
|
|
||||||
properties: {
|
properties: {
|
||||||
identifier: {
|
identifier: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'identifier,后面请求必须带 X-LSP-Idenitifier: <token> 请求头',
|
description: 'identifier,后面请求必须带 X-LSP-Idenitifier: <token> 请求头'
|
||||||
example: '<token>'
|
|
||||||
},
|
},
|
||||||
textures: {
|
textures: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: '用户皮肤和披风',
|
description: '用户皮肤和披风',
|
||||||
example: {
|
|
||||||
skin: '<url>',
|
|
||||||
cape: '<url>'
|
|
||||||
},
|
|
||||||
properties: {
|
properties: {
|
||||||
skin: {
|
skin: {
|
||||||
type: 'string',
|
anyOf: [
|
||||||
description: '用户皮肤',
|
{ type: 'string', description: '皮肤' },
|
||||||
example: '<url>',
|
{ type: 'null' }
|
||||||
optional: true,
|
]
|
||||||
},
|
},
|
||||||
cape: {
|
cape: {
|
||||||
type: 'string',
|
anyOf: [
|
||||||
description: '用户披风',
|
{ type: 'string', description: '披风' },
|
||||||
example: '<url>',
|
{ type: 'null' }
|
||||||
optional: true,
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
username: {
|
username: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '用户名',
|
description: '用户名'
|
||||||
example: '<username>'
|
|
||||||
},
|
},
|
||||||
uuid: {
|
uuid: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '用户唯一标识',
|
description: '用户唯一标识'
|
||||||
example: '<uuid>'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,13 +127,11 @@ export const login = {
|
||||||
properties: {
|
properties: {
|
||||||
err: {
|
err: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: '错误类型',
|
description: '错误类型'
|
||||||
example: 1.048596
|
|
||||||
},
|
},
|
||||||
msg: {
|
msg: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '错误内容,展示给用户看的',
|
description: '错误内容,展示给用户看的'
|
||||||
example: "您输入的密码似乎是用户 lama 的,请确保用户名没有打错!"
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +149,7 @@ export const login = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!user.permissions.some((it) => { s
|
if(!user.permissions.some((it) => {
|
||||||
return it.node === 'login' && it.allowed && (it.duration === 0 || it.startDate + it.duration > Date.now())
|
return it.node === 'login' && it.allowed && (it.duration === 0 || it.startDate + it.duration > Date.now())
|
||||||
})) {
|
})) {
|
||||||
return await rep.code(401).send({
|
return await rep.code(401).send({
|
||||||
|
@ -195,42 +201,42 @@ export const register = {
|
||||||
properties: {
|
properties: {
|
||||||
username: {
|
username: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '用户名',
|
description: '用户名'
|
||||||
example: 'test'
|
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '密码',
|
description: '密码'
|
||||||
example: '123456'
|
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '邮箱',
|
description: '邮箱'
|
||||||
example: ''
|
|
||||||
},
|
},
|
||||||
telegramId: {
|
telegramId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'telegramId',
|
description: 'telegramId'
|
||||||
example: ''
|
|
||||||
},
|
},
|
||||||
textureMigrations: {
|
textureMigrations: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: '纹理迁移',
|
description: '纹理迁移',
|
||||||
optional: true,
|
|
||||||
properties: {
|
properties: {
|
||||||
skin: {
|
skin: {
|
||||||
type: 'string',
|
anyOf: [
|
||||||
description: '皮肤',
|
{ type: 'string', description: '皮肤' },
|
||||||
optional: true,
|
{ type: 'null' }
|
||||||
example: 'https://assets.lama.icu/textures/skin/steve.png'
|
]
|
||||||
},
|
},
|
||||||
cape: {
|
cape: {
|
||||||
type: 'string',
|
anyOf: [
|
||||||
description: '披风',
|
{ type: 'string', description: '披风' },
|
||||||
optional: true,
|
{ type: 'null' }
|
||||||
example: 'https://assets.lama.icu/textures/cape/steve.png'
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -242,42 +248,42 @@ export const register = {
|
||||||
extra: {
|
extra: {
|
||||||
username: {
|
username: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '用户名',
|
description: '用户名'
|
||||||
example: 'test'
|
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '密码',
|
description: '密码'
|
||||||
example: '123456'
|
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: '邮箱',
|
description: '邮箱'
|
||||||
example: ''
|
|
||||||
},
|
},
|
||||||
telegramId: {
|
telegramId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'telegramId',
|
description: 'telegramId'
|
||||||
example: ''
|
|
||||||
},
|
},
|
||||||
textureMigrations: {
|
textureMigrations: {
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description: '纹理迁移',
|
description: '纹理迁移',
|
||||||
optional: true,
|
|
||||||
properties: {
|
properties: {
|
||||||
skin: {
|
skin: {
|
||||||
type: 'string',
|
anyOf: [
|
||||||
description: '皮肤',
|
{ type: 'string', description: '皮肤' },
|
||||||
optional: true,
|
{ type: 'null' }
|
||||||
example: 'https://assets.lama.icu/textures/skin/steve.png'
|
]
|
||||||
},
|
},
|
||||||
cape: {
|
cape: {
|
||||||
type: 'string',
|
anyOf: [
|
||||||
description: '披风',
|
{ type: 'string', description: '披风' },
|
||||||
optional: true,
|
{ type: 'null' }
|
||||||
example: 'https://assets.lama.icu/textures/cape/steve.png'
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ type: 'null' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -337,3 +343,125 @@ export const register = {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const textures = {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/textures',
|
||||||
|
schema: {
|
||||||
|
summary: "获取材质",
|
||||||
|
description: `获取材质,200正确返回 <<< Copilot自己补全的`,
|
||||||
|
tags: [ 'webapi' ],
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...BASE_RESPONSE,
|
||||||
|
extra: {
|
||||||
|
textures: {
|
||||||
|
type: 'object',
|
||||||
|
description: '纹理',
|
||||||
|
properties: {
|
||||||
|
skin: {
|
||||||
|
type: 'string',
|
||||||
|
description: '皮肤',
|
||||||
|
example: 'https://assets.lama.icu/textures/skin/steve.png'
|
||||||
|
},
|
||||||
|
cape: {
|
||||||
|
type: 'string',
|
||||||
|
description: '披风',
|
||||||
|
example: 'https://assets.lama.icu/textures/cape/steve.png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preHandler: getOverridePreHandler('/api/textures') ?? identifierValidator,
|
||||||
|
handler: getOverrideHandler('/api/textures') ?? async function(req, rep) {
|
||||||
|
return await rep.code(200).send({
|
||||||
|
err: 1.048596,
|
||||||
|
msg: '',
|
||||||
|
extra: {
|
||||||
|
textures: {
|
||||||
|
skin: req.user.textures.skin,
|
||||||
|
cape: req.user.textures.cape
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadTexture = {
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/api/textures/:type',
|
||||||
|
schema: {
|
||||||
|
summary: "上传材质",
|
||||||
|
description: `上传材质,200正确返回 <<< Copilot自己补全的`,
|
||||||
|
tags: [ 'webapi' ],
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
...BASE_RESPONSE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...BASE_RESPONSE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
preHandler: getOverridePreHandler('/api/textures') ?? identifierValidator,
|
||||||
|
handler: getOverrideHandler('/api/textures') ?? async function(req, rep) {
|
||||||
|
const { type } = req.params
|
||||||
|
|
||||||
|
if(type !== 'skin' && type !== 'cape') {
|
||||||
|
rep.code(400).send({
|
||||||
|
err: 1.143688,
|
||||||
|
msg: "请求格式不正确"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [hash, texture] = await ImageSecurity.createImageHash(req.body, type === 'cape')
|
||||||
|
req.log.info(`Hash: ${hash}`)
|
||||||
|
|
||||||
|
const buf = await new Promise((resolve, reject) => {
|
||||||
|
const buff = []
|
||||||
|
texture.on("data", (d) => buff.push(d))
|
||||||
|
texture.on("end", () => resolve(Buffer.concat(buff)))
|
||||||
|
texture.on("error", (e) => reject(e))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
await s3Instance.send(new PutObjectCommand({
|
||||||
|
Bucket: config.storage.bucket,
|
||||||
|
Key: `${type}/${hash}`,
|
||||||
|
Body: buf,
|
||||||
|
ACL: 'public-read'
|
||||||
|
}))
|
||||||
|
|
||||||
|
req.log.info(`玩家: ${req.player.username} 上传皮肤,哈希为: ${hash} 成功上传到S3 Key: ${type}/${hash}`)
|
||||||
|
|
||||||
|
const update = { }
|
||||||
|
update[type] = `${type}/${hash}`
|
||||||
|
req.log.info(JSON.stringify({
|
||||||
|
'$set': {
|
||||||
|
textures: update
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
await Player.updateOne({ username: req.player.username }, {
|
||||||
|
'$set': {
|
||||||
|
textures: update
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await rep.code(200).send({
|
||||||
|
err: 1.048596,
|
||||||
|
msg: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,23 @@
|
||||||
import { PNG } from 'pngjs'
|
import { PNG } from 'pngjs'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { createHash, createPrivateKey, createSign } from 'crypto'
|
import { createHash, createPrivateKey, createSign } from 'crypto'
|
||||||
import { config } from './config.js'
|
|
||||||
import { server } from './index.js'
|
import { server } from './index.js'
|
||||||
|
|
||||||
export const ImageSecurity = {
|
export const ImageSecurity = {
|
||||||
createImageHash: async (path, isCape) => {
|
createImageHash: async (stream, isCape) => {
|
||||||
const png = await new Promise((resolve, reject) => {
|
const png = await new Promise((resolve, reject) => {
|
||||||
fs.createReadStream(path).pipe(new PNG()).on('metadata', function(metadata) {
|
stream.pipe(new PNG()).on('metadata', function(metadata) {
|
||||||
if(metadata.width * metadata.height > 40960) {
|
if(metadata.width * metadata.height > 40960) {
|
||||||
reject('Image size exceeds 40960 pixels')
|
reject('Image size exceeds 40960 pixels')
|
||||||
}
|
}
|
||||||
}).on('parsed', function(buff) {
|
}).on('parsed', function(_) {
|
||||||
resolve(this)
|
resolve(this)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isCape) {
|
if (isCape) {
|
||||||
if(png.width % 64 === 0 && png.height % 32 === 0) {
|
if(png.width % 64 === 0 && png.height % 32 === 0) {
|
||||||
return createHashInternal(png)
|
return [createHashInternal(png), stream]
|
||||||
} else if (png.width % 22 === 0 && png.height % 17 === 0) {
|
} else if (png.width % 22 === 0 && png.height % 17 === 0) {
|
||||||
const newPNG = new PNG({
|
const newPNG = new PNG({
|
||||||
width: Math.ceil(png.width / 22),
|
width: Math.ceil(png.width / 22),
|
||||||
|
@ -45,7 +44,7 @@ export const ImageSecurity = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return createHashInternal(newPNG)
|
return [createHashInternal(newPNG), newPNG.pack()]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(png.width % 64 === 0 && png.height % 32 === 0) {
|
if(png.width % 64 === 0 && png.height % 32 === 0) {
|
||||||
|
@ -64,7 +63,7 @@ export const ImageSecurity = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return createHashInternal(png)
|
return [createHashInternal(png), newPNG.pack()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue