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': ''`, 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: 请求头' }, 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': '',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() } }