UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

318 lines (277 loc) 10.8 kB
import debug from 'debug'; import { cookies } from 'next/headers'; import { NextRequest } from 'next/server'; import { IncomingMessage, ServerResponse } from 'node:http'; import urlJoin from 'url-join'; import { appEnv } from '@/envs/app'; const log = debug('lobe-oidc:http-adapter'); /** * 将 Next.js 请求头转换为标准 Node.js HTTP 头格式 */ export const convertHeadersToNodeHeaders = (nextHeaders: Headers): Record<string, string> => { const headers: Record<string, string> = {}; nextHeaders.forEach((value, key) => { headers[key] = value; }); return headers; }; /** * 创建用于 OIDC Provider 的 Node.js HTTP 请求对象 * @param req Next.js 请求对象 */ export const createNodeRequest = async (req: NextRequest): Promise<IncomingMessage> => { // 构建 URL 对象 const url = new URL(req.url); // 计算相对于前缀的路径 let providerPath = url.pathname; // 确保路径始终以/开头 if (!providerPath.startsWith('/')) { providerPath = '/' + providerPath; } log('Creating Node.js request from Next.js request'); log('Original path: %s, Provider path: %s', url.pathname, providerPath); // Attempt to parse and attach body for relevant methods let parsedBody: any = undefined; const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE']; if (methodsWithBody.includes(req.method)) { const contentType = req.headers.get('content-type')?.split(';')[0]; // Get content type without charset etc. log(`Attempting to parse body for ${req.method} with Content-Type: ${contentType}`); try { // Check if body exists and has size before attempting to parse if (req.body && req.headers.get('content-length') !== '0') { if (contentType === 'application/x-www-form-urlencoded') { const formData = await req.formData(); parsedBody = {}; formData.forEach((value, key) => { // If a key appears multiple times, keep the last one (standard form behavior) // Or convert to array if oidc-provider expects it: // if (parsedBody[key]) { // if (!Array.isArray(parsedBody[key])) parsedBody[key] = [parsedBody[key]]; // parsedBody[key].push(value); // } else { // parsedBody[key] = value; // } parsedBody[key] = value; }); log('Parsed form data body: %O', parsedBody); } else if (contentType === 'application/json') { parsedBody = await req.json(); log('Parsed JSON body: %O', parsedBody); } else { log(`Body parsing skipped for Content-Type: ${contentType}. Trying text() as fallback.`); // Fallback: try reading as text if content type is unknown but body exists parsedBody = await req.text(); log('Parsed body as text fallback.'); } } else { log('Request has no body or content-length is 0, skipping parsing.'); } } catch (error) { log('Error parsing request body: %O', error); // Keep parsedBody as undefined, let oidc-provider handle the potential issue } } const nodeRequest = { // 基本属性 headers: convertHeadersToNodeHeaders(req.headers), method: req.method, // 模拟可读流行为 (oidc-provider might not rely on this if body is pre-parsed) // eslint-disable-next-line @typescript-eslint/ban-types on: (event: string, handler: Function) => { if (event === 'end') { // Simulate end immediately as body is already processed or will be attached handler(); } }, // 添加 Node.js 服务器所期望的额外属性 socket: { remoteAddress: req.headers.get('x-forwarded-for') || '127.0.0.1', }, url: providerPath + url.search, ...(parsedBody !== undefined && { body: parsedBody }), // Attach body if it exists }; log('Node.js request created with method %s and path %s', nodeRequest.method, nodeRequest.url); if (nodeRequest.body) { log('Attached parsed body to Node.js request.'); } // Cast back to IncomingMessage for the function's return signature return nodeRequest as unknown as IncomingMessage; }; /** * 响应收集器接口,用于捕获 OIDC Provider 的响应 */ export interface ResponseCollector { nodeResponse: ServerResponse; readonly responseBody: string | Buffer; readonly responseHeaders: Record<string, string | string[]>; readonly responseStatus: number; } /** * 创建用于 OIDC Provider 的 Node.js HTTP 响应对象 * @param resolvePromise 当响应完成时调用的解析函数 */ export const createNodeResponse = (resolvePromise: () => void): ResponseCollector => { log('Creating Node.js response collector'); // 存储响应状态的对象 const state = { responseBody: '' as string | Buffer, responseHeaders: {} as Record<string, string | string[]>, responseStatus: 200, }; let promiseResolved = false; const nodeResponse: any = { end: (chunk?: string | Buffer) => { log('NodeResponse.end called'); if (chunk) { log('NodeResponse.end chunk: %s', typeof chunk === 'string' ? chunk : '(Buffer)'); // @ts-ignore state.responseBody += chunk; } const locationHeader = state.responseHeaders['location']; if (locationHeader && state.responseStatus === 200) { log('Location header detected with status 200, overriding to 302'); state.responseStatus = 302; } if (!promiseResolved) { log('Resolving response promise'); promiseResolved = true; resolvePromise(); } }, getHeader: (name: string) => { const lowerName = name.toLowerCase(); return state.responseHeaders[lowerName]; }, getHeaderNames: () => { return Object.keys(state.responseHeaders); }, getHeaders: () => { return state.responseHeaders; }, headersSent: false, removeHeader: (name: string) => { const lowerName = name.toLowerCase(); log('Removing header: %s', lowerName); delete state.responseHeaders[lowerName]; }, setHeader: (name: string, value: string | string[]) => { const lowerName = name.toLowerCase(); log('Setting header: %s = %s', lowerName, value); state.responseHeaders[lowerName] = value; }, write: (chunk: string | Buffer) => { log('NodeResponse.write called with chunk'); // @ts-ignore state.responseBody += chunk; }, writeHead: (status: number, headers?: Record<string, string | string[]>) => { log('NodeResponse.writeHead called with status: %d', status); state.responseStatus = status; if (headers) { const lowerCaseHeaders = Object.entries(headers).reduce( (acc, [key, value]) => { acc[key.toLowerCase()] = value; return acc; }, {} as Record<string, string | string[]>, ); state.responseHeaders = { ...state.responseHeaders, ...lowerCaseHeaders }; } (nodeResponse as any).headersSent = true; }, } as unknown as ServerResponse; log('Node.js response collector created successfully'); return { nodeResponse, get responseBody() { return state.responseBody; }, get responseHeaders() { return state.responseHeaders; }, get responseStatus() { return state.responseStatus; }, }; }; /** * 创建用于调用 provider.interactionDetails 的上下文 (req, res) * @param uid 交互 ID */ export const createContextForInteractionDetails = async ( uid: string, ): Promise<{ req: IncomingMessage; res: ServerResponse }> => { log('Creating context for interaction details for uid: %s', uid); const baseUrl = appEnv.APP_URL!; log('Using base URL: %s', baseUrl); // 从baseUrl提取主机名和协议用于headers const parsedUrl = new URL(baseUrl); const hostName = parsedUrl.host; const protocol = parsedUrl.protocol.replace(':', ''); // 1. 获取真实的 Cookies const cookieStore = await cookies(); const realCookies: Record<string, string> = {}; cookieStore.getAll().forEach((cookie) => { realCookies[cookie.name] = cookie.value; }); log('Real cookies found: %o', Object.keys(realCookies)); // 特别检查交互会话cookie const interactionCookieName = `_interaction_${uid}`; if (realCookies[interactionCookieName]) { log('Found interaction session cookie: %s', interactionCookieName); } else { log('Warning: Interaction session cookie not found: %s', interactionCookieName); } // 2. 构建包含真实 Cookie 的 Headers const headers = new Headers({ 'host': hostName, 'x-forwarded-host': hostName, 'x-forwarded-proto': protocol, }); const cookieString = Object.entries(realCookies) .map(([name, value]) => `${name}=${value}`) .join('; '); if (cookieString) { headers.set('cookie', cookieString); log('Setting cookie header'); } else { log('No cookies found to set in header'); } // 3. 创建模拟的 NextRequest // 注意:这里的 IP, geo, ua 等信息可能是 oidc-provider 某些特性需要的, // 如果遇到相关问题,可能需要从真实请求头中提取 (e.g., 'x-forwarded-for', 'user-agent') const interactionUrl = urlJoin(baseUrl, `/oauth/consent/${uid}`); log('Creating interaction URL: %s', interactionUrl); const mockNextRequest = { cookies: { // 模拟 NextRequestCookies 接口 get: (name: string) => cookieStore.get(name)?.value, getAll: () => cookieStore.getAll(), has: (name: string) => cookieStore.has(name), }, geo: {}, headers: headers, ip: '127.0.0.1', method: 'GET', nextUrl: new URL(interactionUrl), page: { name: undefined, params: undefined }, ua: undefined, url: new URL(interactionUrl), } as unknown as NextRequest; log('Mock NextRequest created for url: %s', mockNextRequest.url); // 4. 使用 createNodeRequest 创建模拟的 Node.js IncomingMessage // pathPrefix 设置为 '/' 因为我们的 URL 已经是 Provider 期望的路径格式 /interaction/:uid const req: IncomingMessage = await createNodeRequest(mockNextRequest); // @ts-ignore - 将解析出的 cookies 附加到模拟的 Node.js 请求上 req.cookies = realCookies; log('Node.js IncomingMessage created, attached real cookies'); // 5. 使用 createNodeResponse 创建模拟的 Node.js ServerResponse let resolveFunc: () => void; new Promise<void>((resolve) => { resolveFunc = resolve; }); const responseCollector: ResponseCollector = createNodeResponse(() => resolveFunc()); const res: ServerResponse = responseCollector.nodeResponse; log('Node.js ServerResponse created'); return { req, res }; };