diff --git a/adapters/lark/src/bot.ts b/adapters/lark/src/bot.ts index 11f97b44..fd3ed07a 100644 --- a/adapters/lark/src/bot.ts +++ b/adapters/lark/src/bot.ts @@ -26,8 +26,20 @@ export class LarkBot extends Bot ctx.plugin(HttpServer, this) - this.defineInternalRoute('/*path', async ({ params, method, headers, body }) => { - return this.http(params.path, { method, data: body, headers }) + this.defineInternalRoute('/*path', async ({ params, method, headers, body, query }) => { + const response = await this.http('/' + params.path, { + method, + headers, + data: method === 'GET' || method === 'HEAD' ? null : body, + params: Object.fromEntries(query.entries()), + responseType: 'arraybuffer', + validateStatus: () => true, + }) + return { + status: response.status, + body: response.data, + headers: response.headers, + } }) } @@ -163,14 +175,14 @@ export namespace LarkBot { Schema.object({ platform: Schema.const('feishu').required(), }), - HTTP.createConfig('https://open.feishu.cn/open-apis/'), + HTTP.createConfig('https://open.feishu.cn/open-apis'), HttpServer.createConfig('/feishu'), ]), Schema.intersect([ Schema.object({ platform: Schema.const('lark').required(), }), - HTTP.createConfig('https://open.larksuite.com/open-apis/'), + HTTP.createConfig('https://open.larksuite.com/open-apis'), HttpServer.createConfig('/lark'), ]), ]), diff --git a/adapters/satori/src/bot.ts b/adapters/satori/src/bot.ts index 7964e6eb..22571bb9 100644 --- a/adapters/satori/src/bot.ts +++ b/adapters/satori/src/bot.ts @@ -31,7 +31,7 @@ function createInternal(bot: SatoriBot, prefix = '') { const blobs: Dict = Object.create(null) const data = serialize(args, '$', blobs) if (!Object.keys(blobs).length) { - return bot.http.post('/v1/internal/' + key, args) + return bot.http.post('/v1/internal/_api/' + key, args) } const form = new FormData() form.append('$', JSON.stringify(data)) @@ -42,7 +42,7 @@ function createInternal(bot: SatoriBot, prefix = '') { form.append(key, value) } } - return bot.http.post('/v1/internal/' + key, form) + return bot.http.post('/v1/internal/_api/' + key, form) }, get(target, key, receiver) { if (typeof key === 'symbol' || key in target) { @@ -61,11 +61,19 @@ export class SatoriBot extends Bot { - return await this.http(`/v1/${this.getInternalUrl(params.path, query, true)}`, { + this.defineInternalRoute('/*path', async ({ method, params, query, headers, body }) => { + const response = await this.http(`/v1/${this.getInternalUrl('/' + params.path, query, true)}`, { method, + headers, + data: method === 'GET' || method === 'HEAD' ? null : body, responseType: 'arraybuffer', + validateStatus: () => true, }) + return { + status: response.status, + body: response.data, + headers: response.headers, + } }) } } diff --git a/packages/server/package.json b/packages/server/package.json index d98ed07f..2050decd 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -35,12 +35,15 @@ "api" ], "devDependencies": { - "@cordisjs/plugin-server": "^0.2.4", + "@cordisjs/plugin-server": "^0.2.5", "@satorijs/core": "^4.3.1", "@types/node": "^22.7.5", "cordis": "^3.18.1" }, "peerDependencies": { "@satorijs/core": "^4.3.1" + }, + "dependencies": { + "parse-multipart-data": "^1.5.0" } } diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 191b8849..68bb1a0b 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,9 +1,11 @@ -import { Binary, camelCase, Context, Dict, makeArray, sanitize, Schema, Service, Session, snakeCase, Time, Universal, valueMap } from '@satorijs/core' +import { Context, Schema, Service, Session, Universal } from '@satorijs/core' +import { Binary, camelCase, defineProperty, Dict, makeArray, sanitize, snakeCase, Time, valueMap } from 'cosmokit' import {} from '@cordisjs/plugin-server' import WebSocket from 'ws' import { Readable } from 'node:stream' import { readFile } from 'node:fs/promises' -import { ParameterizedContext } from 'koa' +import { Middleware, ParameterizedContext } from 'koa' +import { getBoundary, parse } from 'parse-multipart-data' declare module 'cordis' { interface Context { @@ -43,6 +45,15 @@ function deserialize(data: any, path: string, blobs: Dict) { }) } +const FILTER_HEADERS = [ + 'host', + 'authorization', + 'satori-user-id', + 'satori-platform', + 'x-self-id', + 'x-platform', +] + class SatoriServer extends Service { static inject = ['server', 'http'] @@ -51,6 +62,34 @@ class SatoriServer extends Service { const logger = ctx.logger('server') const path = sanitize(config.path) + ctx.satori.defineInternalRoute('/_api/:name', async ({ bot, headers, params, method, body }) => { + if (method !== 'POST') return { status: 405 } + const type = headers['content-type'] + const boundary = getBoundary(type) + let args: any + if (boundary) { + const blobs: Dict = {} + const fields: Dict = {} + for (const { name, type, data, filename } of parse(Buffer.from(body), boundary)) { + if (type) { + blobs[name!] = new File([data], filename!, { type }) + } else { + fields[name!] = data.toString() + } + } + args = deserialize(JSON.parse(fields.$), '$', blobs) + } else { + args = JSON.parse(new TextDecoder().decode(body)) + } + try { + const result = await bot.internal[camelCase(params.name)](...args) + return { body: result, status: 200 } + } catch (error) { + if (!ctx.http.isError(error) || !error.response) throw error + return error.response + } + }) + function checkAuth(koa: ParameterizedContext) { if (!config.token) return if (koa.request.headers.authorization !== `Bearer ${config.token}`) { @@ -113,45 +152,33 @@ class SatoriServer extends Service { koa.status = 200 }) - ctx.server.post(path + '/v1/internal/:name', async (koa) => { - if (checkAuth(koa)) return + const marker: Middleware = defineProperty((_, next) => next(), Symbol.for('noParseBody'), true) - const selfId = koa.request.headers['x-self-id'] - const platform = koa.request.headers['x-platform'] - const bot = ctx.bots.find(bot => bot.selfId === selfId && bot.platform === platform) - if (!bot) { - koa.body = 'login not found' - koa.status = 403 - return + ctx.server.all(path + '/v1/internal/:path(.+)', marker, async (koa) => { + const url = new URL(`internal:${koa.params.path}`) + for (const [key, value] of Object.entries(koa.query)) { + for (const item of makeArray(value)) { + url.searchParams.append(key, item) + } } - const name = camelCase(koa.params.name) - if (!bot.internal?.[name]) { - koa.body = 'method not found' - koa.status = 404 - return + const headers = new Headers() + for (const [key, value] of Object.entries(koa.headers)) { + if (FILTER_HEADERS.includes(key)) continue + headers.set(key, value as string) } - try { - let args = koa.request.body - if (koa.request.files) { - const blobs = Object.fromEntries(await Promise.all(Object.entries(koa.request.files).map(async ([key, value]) => { - value = makeArray(value)[0] - const buffer = await readFile(value.filepath) - return [key, new File([buffer], value.originalFilename!, { type: value.mimetype! })] as const - }))) - args = deserialize(JSON.parse(koa.request.body.$), '$', blobs) - } - const result = await bot.internal[name](...args) - koa.body = result - koa.status = 200 - } catch (error) { - if (!ctx.http.isError(error) || !error.response) throw error - koa.status = error.response.status - koa.body = error.response.data - for (const [key, value] of error.response.headers) { - koa.set(key, value) - } + + const buffers: any[] = [] + for await (const chunk of koa.req) { + buffers.push(chunk) + } + const body = Binary.fromSource(Buffer.concat(buffers)) + const response = await ctx.satori.handleInternalRoute(koa.method as any, url, headers, body) + for (const [key, value] of response.headers ?? new Headers()) { + koa.set(key, value) } + koa.status = response.status + koa.body = response.body ? Buffer.from(response.body) : '' }) ctx.server.get(path + '/v1/proxy/:url(.+)', async (koa) => { @@ -165,34 +192,21 @@ class SatoriServer extends Service { } koa.header['Access-Control-Allow-Origin'] = ctx.server.config.selfUrl || '*' - if (url.protocol === 'internal:') { - const { status, statusText, body, headers } = await ctx.satori.handleInternalRoute('GET', url) - koa.status = status - for (const [key, value] of headers || new Headers()) { - koa.set(key, value) - } - if (status >= 200 && status < 300) { - koa.body = body ? Buffer.from(body) : null - } else { - koa.body = statusText - } - } else { - const proxyUrls = ctx.bots.flatMap(bot => bot.proxyUrls) - if (!proxyUrls.some(proxyUrl => url.href.startsWith(proxyUrl))) { - koa.body = 'forbidden' - koa.status = 403 - return - } + const proxyUrls = ctx.bots.flatMap(bot => bot.proxyUrls) + if (!proxyUrls.some(proxyUrl => url.href.startsWith(proxyUrl))) { + koa.body = 'forbidden' + koa.status = 403 + return + } - try { - koa.body = Readable.fromWeb(await ctx.http.get(url.href, { responseType: 'stream' })) - } catch (error) { - if (!ctx.http.isError(error) || !error.response) throw error - koa.status = error.response.status - koa.body = error.response.data - for (const [key, value] of error.response.headers) { - koa.set(key, value) - } + try { + koa.body = Readable.fromWeb(await ctx.http.get(url.href, { responseType: 'stream' })) + } catch (error) { + if (!ctx.http.isError(error) || !error.response) throw error + koa.status = error.response.status + koa.body = error.response.data + for (const [key, value] of error.response.headers) { + koa.set(key, value) } } })