lsp-yggdrasil/src/routes/web-api.js

644 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { config, getOverrideHandler, getOverridePreHandler } from "../config.js"
import { Player } from "../models/player.js"
import { createHash } from "crypto"
import { generateToken, uuid } from "../generator.js"
import { Token } from "../models/token.js"
import { s3Instance, server } from "../index.js"
import { ImageSecurity } from "../secure.js"
import { PutObjectCommand } from "@aws-sdk/client-s3"
import crypto from 'crypto'
const defaultSkin = "https://textures.minecraft.net/texture/ddb8684e59f771666bde5f411fcb2e495c452f2ecabc31981bc132ac71bdd394"
const BASE_RESPONSE = {
err: {
type: "number",
description: "错误类型, 1.048596代表无错误",
example: 1.048596
},
msg: {
type: "string",
description: "错误信息,如果有错误则返回错误信息,否则返回空字符串 << 感谢 Copilot 的补全",
example: "你号被ban了"
}
}
const identifiers = new Map()
async function identifierValidator(req, rep) {
const identifier = req.headers['x-lsp-identifier']
if(!identifier) {
return await rep.code(200).send({
err: 1.143688,
msg: "请求格式不正确"
})
}
if(!identifiers.has(identifier)) {
return await rep.code(200).send({
err: 0.456914,
msg: "用户不存在"
})
}
const {t, uuid} = identifiers.get(identifier)
if(t < Date.now()) {
return await rep.code(200).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 = {
method: 'POST',
url: '/api/login',
schema: {
summary: "登录",
description: `登录到 webapi后续请求需要携带请求头 'X-LSP-Idenitifier': '<token>'`,
tags: [ 'webapi' ],
body: {
type: 'object',
properties: {
username: {
type: 'string',
description: '用户名'
},
password: {
type: 'string',
description: '密码'
},
createToken: {
type: 'boolean',
description: '是否创建一个 accessToken'
}
}
},
response: {
200: {
type: 'object',
properties: {
...BASE_RESPONSE,
extra: {
type: 'object',
description: '额外信息',
properties: {
identifier: {
type: 'string',
description: 'identifier后面请求必须带 X-LSP-Idenitifier: <token> 请求头'
},
textures: {
type: 'object',
description: '用户皮肤和披风',
properties: {
skin: {
anyOf: [
{ type: 'string', description: '皮肤' },
{ type: 'null' }
]
},
cape: {
anyOf: [
{ type: 'string', description: '披风' },
{ type: 'null' }
]
}
},
},
username: {
type: 'string',
description: '用户名'
},
uuid: {
type: 'string',
description: '用户唯一标识'
}
}
}
}
},
401: {
type: 'object',
properties: {
err: {
type: 'number',
description: '错误类型'
},
msg: {
type: 'string',
description: '错误内容,展示给用户看的'
},
}
}
},
},
preHandler: getOverridePreHandler('/api/login'),
handler: getOverrideHandler('/api/login') ?? async function(req, rep) {
const { username, password, createToken } = req.body;
const user = await Player.findOne({ email: username, password: createHash("sha256").update(password).digest('hex') });
if (!user) {
return rep.code(200).send({
err: 1.143688,
msg: "用户名或密码错误"
});
}
if(user.permissions.indexOf("login") === -1) {
return await rep.code(200).send({
err: 0.337187,
msg: "泻药,宁滴账号已被封禁"
});
}
const [token, key] = generateToken(`webapi:${user.username}`)
this.log.info(`/api/login > 为玩家 webapi:${user.username} 生成令牌: ${token} | 随机 key = ${key}`)
identifiers.set(token, {
uuid: user.uuid,
t: Date.now() + 1000 * 60 * 60 * 24 * 1,
})
if(createToken) {
new Token({
uuid: user.uuid,
token: token,
clientToken: `${req.headers['x-forwarded-for'] || req.ip}:${token.substring(3, 8)}`,
expireDate: Date.now() + 1000 * 60 * 60 * 24 * 15,
deadDate: Date.now() + 1000 * 60 * 60 * 24 * 30,
}).save()
}
return await rep.code(200).send(JSON.stringify({
err: 1.048596,
msg: '',
extra: {
identifier: token,
textures: user.textures,
username: user.username,
uuid: user.uuid,
}
}))
}
}
export const register = {
method: 'POST',
url: '/api/register',
schema: {
summary: "注册",
description: `注册到 webapi后续请求需要先登录获取identifier然后携带请求头 'X-LSP-Idenitifier': '<token>'200正确返回 <<< Copilot自己补全的`,
tags: [ 'webapi' ],
body: {
type: 'object',
properties: {
username: {
type: 'string',
description: '用户名'
},
password: {
type: 'string',
description: '密码'
},
email: {
type: 'string',
description: '邮箱'
},
invitationCode: {
type: 'string',
description: 'invitationCode'
},
validationCode: {
type: 'string',
description: 'invitationCode'
},
textureMigrations: {
anyOf: [
{
type: 'object',
description: '纹理迁移',
properties: {
skin: {
anyOf: [
{ type: 'string', description: '皮肤' },
{ type: 'null' }
]
},
cape: {
anyOf: [
{ type: 'string', description: '披风' },
{ type: 'null' }
]
}
}
},
{ type: 'null' }
]
}
}
},
response: {
200: {
type: 'object',
properties: {
...BASE_RESPONSE
}
},
},
},
preHandler: getOverridePreHandler('/api/register'),
handler: getOverrideHandler('/api/register') ?? async function(req, rep) {
const { username, password, email, invitationCode, textureMigrations, validationCode } = req.body
const user = await Player.findOne({ $or: [
{ email: email }, { username: username }
] })
if (user) {
return await rep.code(200).send({
err: 1.143688,
msg: "用户名已存在"
})
}
if(username == 0 || password == 0 || email == 0 || invitationCode == 0 || validationCode == 0) {
return await rep.code(200).send({
err: 1.143688,
msg: "用户名/密码/邮箱/telegramId不能为空"
})
}
const textues = { }
if(textureMigrations) {
textues.skin = textureMigrations.skin ?? defaultSkin
if(textureMigrations.cape != 0 && textureMigrations.cape) {
textues.cape = textureMigrations.cape
}
}
/*
Examples:
{
p(latform): "name",
n(ame): "name",
t(o): "email",
}
v -> Signature
*/
const raw = Buffer.from(invitationCode, 'base64').toString().split(';').filter(it => it.indexOf('=') >= 0)
const fields = new Map()
raw.forEach(kvPair => {
const [k, v] = kvPair.split('=', 2)
req.log.info(`k: ${k} v: ${v}`)
fields.set(k, v)
})
if(!crypto.createVerify('rsa-sha1').update(Buffer.from(invitationCode)).verify(server.keys.publicKey, Buffer.from(validationCode, 'hex'))) {
return await rep.code(200).send({
err: 1.143688,
msg: "邀请码验证失败!非法邀请码!"
})
}
if(fields.get('t') !== email) {
return await rep.code(200).send({
err: 1.143688,
msg: "邀请码验证失败!这邀请码不属于你!"
})
}
const newUser = new Player({
username,
password: createHash("sha256").update(password).digest('hex'),
email,
uuid: uuid('LSPlayer:' + email),
textues,
registerDate: Date.now(),
permissions: ['login'],
binding: {
platform: fields.get('p'),
username: fields.get('n'),
verified: true,
}
});
await newUser.save()
return await rep.code(200).send({
err: 1.048596,
msg: '',
})
}
}
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(200).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: JSON.stringify(update)
})
}
}
export const telegramBind = {
method: 'GET',
url: '/api/binding/:uuid',
schema: {
summary: "获取 Telegram 绑定",
description: "获取玩家对应 Telegram 绑定,返回纯文本。",
tags: [ "webapi" ],
response: {
200: {
type: 'object',
properties: {
username: {
type: 'string'
},
platform: {
type: 'string'
},
verified: {
type: 'boolean'
}
}
}
}
},
preHandler: getOverridePreHandler('/api/binding/:uuid'),
handler: getOverrideHandler('/api/binding/:uuid') ?? async function (req, rep) {
const { uuid } = req.params
return await rep.code(200).send((await Player.findOne({ uuid }))?.binding ?? { username: '', platform: '', verified: false })
}
}
export const meta = {
method: "GET",
url: "/",
schema: {
summary: "获取 Meta 信息",
description: "获取服务器元数据。详情请见 authlib-injector 文档。",
tags: [ "api" ],
response: {
200: {
"type": "object",
"properties": {
"meta": {
"type": "object",
"properties": {
"serverName": {
"type": "string"
},
"implementationName": {
"type": "string"
},
"implementationVersion": {
"type": "string"
}
}
},
"skinDomains": {
"type": "array",
"items": {
"type": "string"
}
},
"signaturePublickey": {
"type": "string"
}
}
}
}
},
preHandler: getOverridePreHandler("/"),
handler: getOverrideHandler("/") ?? async function(req, rep) {
rep.code(200).send({
meta: {
serverName: config.server.serverName,
implementationName: "lsp-yggdrasil",
implementationVersion: "1.0",
},
skinDomains: config.server.skinDomain,
signaturePublickey: this.keys.publicKey
})
}
}
export const status = {
method: "GET",
url: "/status",
schema: {
summary: "获取服务器信息",
description: "获取服务器基本信息",
tags: [ "webapi" ],
response: {
200: {
"type": "object",
"properties": {
"public": {
"type": "string"
},
"version": {
"type": "string"
},
"api": {
"type": "object",
"properties": {
"yggdrasil": {
"type": "string"
},
"webapi": {
"type": "string"
}
}
},
"name": {
"type": "string"
},
"server": {
"type": "string"
},
"env": {
"type": "object",
"properties": {
"node": {
"type": "string"
},
"os": {
"type": "string"
},
"arch": {
"type": "string"
},
"t": {
"type": "number"
},
}
}
}
}
}
},
preHandler: getOverridePreHandler("/status"),
handler: getOverrideHandler("/status") ?? async function(req, rep) {
rep.code(200).send({
public: this.keys.publicKey,
version: "1.0",
name: config.server.serverName,
api: {
yggdrasil: "authlib-injector",
webapi: "standard-1.0"
},
server: "lsp-yggdrasil",
env: {
"node": process.version,
"os": process.platform,
"arch": process.arch,
"t": Date.now(),
}
})
}
}
export const CORS_BYPASS = {
method: "OPTIONS",
url: "/*",
schema: {
summary: "跨域访问",
description: "跨域访问",
tags: [ "webapi" ],
response: {
200: {
type: "null"
},
},
},
preHandler: getOverridePreHandler("/*"),
handler: getOverrideHandler("/*") ?? function(req, rep) {
rep.header("Access-Control-Allow-Origin", "*").code(200).send()
}
}