feat(grok): 使用 UUID 替换 SSO token 进行资源代理以提高安全性

- 在 Grok 资源代理接口中优先使用 UUID 获取 token,避免 token 泄露在 URL 中
- 为 ProviderPoolManager 添加 findProviderByUuid 方法,支持通过 UUID 查找配置
- 重构 GrokConverter,将 SSO token 依赖改为 UUID 依赖
- 更新 VERSION 文件至 2.10.2.1
This commit is contained in:
hex2077 2026-03-02 00:14:37 +08:00
parent c91d2ce3ab
commit 3f8fdc0b8e
6 changed files with 77 additions and 58 deletions

View file

@ -1 +1 @@
2.10.2 2.10.2.1

View file

@ -13,9 +13,9 @@ import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
* 实现Grok协议到其他协议的转换 * 实现Grok协议到其他协议的转换
*/ */
export class GrokConverter extends BaseConverter { export class GrokConverter extends BaseConverter {
// 静态属性,确保所有实例共享最新的认证和基础 URL 配置 // 静态属性,确保所有实例共享最新的基础 URL 和 UUID 配置
static sharedSsoToken = null;
static sharedRequestBaseUrl = ""; static sharedRequestBaseUrl = "";
static sharedUuid = null;
constructor() { constructor() {
super('grok'); super('grok');
@ -23,20 +23,6 @@ export class GrokConverter extends BaseConverter {
this.requestStates = new Map(); this.requestStates = new Map();
} }
/**
* 设置 Grok SSO token
*/
setSsoToken(token) {
if (!token) return;
// 如果 token 包含 sso= 前缀,则去掉它
let processedToken = token;
if (processedToken.startsWith("sso=")) {
processedToken = processedToken.substring(4);
}
GrokConverter.sharedSsoToken = processedToken;
}
/** /**
* 设置请求的基础 URL * 设置请求的基础 URL
*/ */
@ -47,13 +33,22 @@ export class GrokConverter extends BaseConverter {
} }
/** /**
* assets.grok.com 域名的资源 URL 添加 sso 参数并转换为本地代理 URL * 设置账号的 UUID
*/
setUuid(uuid) {
if (uuid) {
GrokConverter.sharedUuid = uuid;
}
}
/**
* assets.grok.com 域名的资源 URL 添加 uuid 参数并转换为本地代理 URL
*/ */
_appendSsoToken(url, state = null) { _appendSsoToken(url, state = null) {
const ssoToken = state?.ssoToken || GrokConverter.sharedSsoToken;
const requestBaseUrl = state?.requestBaseUrl || GrokConverter.sharedRequestBaseUrl; const requestBaseUrl = state?.requestBaseUrl || GrokConverter.sharedRequestBaseUrl;
const uuid = state?.uuid || GrokConverter.sharedUuid;
if (!url || !ssoToken) return url; if (!url || !uuid) return url;
// 检查是否为 assets.grok.com 域名或相对路径 // 检查是否为 assets.grok.com 域名或相对路径
const isGrokAsset = url.includes('assets.grok.com') || (!url.startsWith('http') && !url.startsWith('data:')); const isGrokAsset = url.includes('assets.grok.com') || (!url.startsWith('http') && !url.startsWith('data:'));
@ -67,7 +62,10 @@ export class GrokConverter extends BaseConverter {
} }
// 返回本地代理接口 URL // 返回本地代理接口 URL
const proxyPath = `/api/grok/assets?url=${encodeURIComponent(originalUrl)}&sso=${encodeURIComponent(ssoToken)}`; // 使用 uuid 以提高安全性,防止 token 泄露在链接中
const authParam = `uuid=${encodeURIComponent(uuid)}`;
const proxyPath = `/api/grok/assets?url=${encodeURIComponent(originalUrl)}&${authParam}`;
if (requestBaseUrl) { if (requestBaseUrl) {
return `${requestBaseUrl}${proxyPath}`; return `${requestBaseUrl}${proxyPath}`;
} }
@ -78,8 +76,8 @@ export class GrokConverter extends BaseConverter {
* 在文本中查找并替换所有 assets.grok.com 的资源链接为绝对代理链接 * 在文本中查找并替换所有 assets.grok.com 的资源链接为绝对代理链接
*/ */
_processGrokAssetsInText(text, state = null) { _processGrokAssetsInText(text, state = null) {
const ssoToken = state?.ssoToken || GrokConverter.sharedSsoToken; const uuid = state?.uuid || GrokConverter.sharedUuid;
if (!text || !ssoToken) return text; if (!text || !uuid) return text;
// 更宽松的正则匹配 assets.grok.com 的 URL // 更宽松的正则匹配 assets.grok.com 的 URL
const grokUrlRegex = /https?:\/\/assets\.grok\.com\/[^\s\)\"\'\>]+/g; const grokUrlRegex = /https?:\/\/assets\.grok\.com\/[^\s\)\"\'\>]+/g;
@ -106,8 +104,8 @@ export class GrokConverter extends BaseConverter {
has_tool_call: false, has_tool_call: false,
rollout_id: "", rollout_id: "",
in_tool_call: false, // 是否处于 <tool_call> 块内 in_tool_call: false, // 是否处于 <tool_call> 块内
ssoToken: null,
requestBaseUrl: "", requestBaseUrl: "",
uuid: null,
pending_text_buffer: "" // 用于处理流式输出中被截断的 URL pending_text_buffer: "" // 用于处理流式输出中被截断的 URL
}); });
} }
@ -345,32 +343,32 @@ export class GrokConverter extends BaseConverter {
/** /**
* 渲染图片为 Markdown * 渲染图片为 Markdown
*/ */
_renderImage(url, imageId = "image") { _renderImage(url, imageId = "image", state = null) {
let finalUrl = url; let finalUrl = url;
if (!url.startsWith('http')) { if (!url.startsWith('http')) {
finalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`; finalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`;
} }
finalUrl = this._appendSsoToken(finalUrl); finalUrl = this._appendSsoToken(finalUrl, state);
return `![${imageId}](${finalUrl})`; return `![${imageId}](${finalUrl})`;
} }
/** /**
* 渲染视频为 Markdown/HTML (render_video) * 渲染视频为 Markdown/HTML (render_video)
*/ */
_renderVideo(videoUrl, thumbnailImageUrl = "") { _renderVideo(videoUrl, thumbnailImageUrl = "", state = null) {
let finalVideoUrl = videoUrl; let finalVideoUrl = videoUrl;
if (!videoUrl.startsWith('http')) { if (!videoUrl.startsWith('http')) {
finalVideoUrl = `https://assets.grok.com${videoUrl.startsWith('/') ? '' : '/'}${videoUrl}`; finalVideoUrl = `https://assets.grok.com${videoUrl.startsWith('/') ? '' : '/'}${videoUrl}`;
} }
finalVideoUrl = this._appendSsoToken(finalVideoUrl); finalVideoUrl = this._appendSsoToken(finalVideoUrl, state);
let finalThumbUrl = thumbnailImageUrl; let finalThumbUrl = thumbnailImageUrl;
if (thumbnailImageUrl && !thumbnailImageUrl.startsWith('http')) { if (thumbnailImageUrl && !thumbnailImageUrl.startsWith('http')) {
finalThumbUrl = `https://assets.grok.com${thumbnailImageUrl.startsWith('/') ? '' : '/'}${thumbnailImageUrl}`; finalThumbUrl = `https://assets.grok.com${thumbnailImageUrl.startsWith('/') ? '' : '/'}${thumbnailImageUrl}`;
} }
finalThumbUrl = this._appendSsoToken(finalThumbUrl); finalThumbUrl = this._appendSsoToken(finalThumbUrl, state);
const defaultThumb = this._appendSsoToken('https://assets.grok.com/favicon.ico'); const defaultThumb = this._appendSsoToken('https://assets.grok.com/favicon.ico', state);
return `\n[![video](${finalThumbUrl || defaultThumb})](${finalVideoUrl})\n[Play Video](${finalVideoUrl})\n`; return `\n[![video](${finalThumbUrl || defaultThumb})](${finalVideoUrl})\n[Play Video](${finalVideoUrl})\n`;
} }
@ -455,16 +453,12 @@ export class GrokConverter extends BaseConverter {
const modelHash = grokResponse.llmInfo?.modelHash || ""; const modelHash = grokResponse.llmInfo?.modelHash || "";
const state = this._getState(this._formatResponseId(responseId)); const state = this._getState(this._formatResponseId(responseId));
if (grokResponse._ssoToken) {
let processedToken = grokResponse._ssoToken;
if (processedToken.startsWith("sso=")) {
processedToken = processedToken.substring(4);
}
state.ssoToken = processedToken;
}
if (grokResponse._requestBaseUrl) { if (grokResponse._requestBaseUrl) {
state.requestBaseUrl = grokResponse._requestBaseUrl; state.requestBaseUrl = grokResponse._requestBaseUrl;
} }
if (grokResponse._uuid) {
state.uuid = grokResponse._uuid;
}
// 过滤内容并处理其中的 Grok 资源链接 // 过滤内容并处理其中的 Grok 资源链接
content = this._filterToken(content, responseId); content = this._filterToken(content, responseId);
@ -534,17 +528,13 @@ export class GrokConverter extends BaseConverter {
const responseId = this._formatResponseId(rawResponseId); const responseId = this._formatResponseId(rawResponseId);
const state = this._getState(responseId); const state = this._getState(responseId);
// 从响应块中同步 token 和基础 URL // 从响应块中同步 uuid 和基础 URL
if (resp._ssoToken) {
let processedToken = resp._ssoToken;
if (processedToken.startsWith("sso=")) {
processedToken = processedToken.substring(4);
}
state.ssoToken = processedToken;
}
if (resp._requestBaseUrl) { if (resp._requestBaseUrl) {
state.requestBaseUrl = resp._requestBaseUrl; state.requestBaseUrl = resp._requestBaseUrl;
} }
if (resp._uuid) {
state.uuid = resp._uuid;
}
if (resp.llmInfo?.modelHash && !state.fingerprint) { if (resp.llmInfo?.modelHash && !state.fingerprint) {
state.fingerprint = resp.llmInfo.modelHash; state.fingerprint = resp.llmInfo.modelHash;

View file

@ -115,7 +115,7 @@ export function createRequestHandler(config, providerPoolManager) {
// Grok assets proxy endpoint // Grok assets proxy endpoint
if (method === 'GET' && path === '/api/grok/assets') { if (method === 'GET' && path === '/api/grok/assets') {
await handleGrokAssetsProxy(req, res, currentConfig); await handleGrokAssetsProxy(req, res, currentConfig, providerPoolManager);
return true; return true;
} }

View file

@ -108,10 +108,10 @@ export class GrokApiService {
this.chatApi = `${this.baseUrl}/rest/app-chat/conversations/new`; this.chatApi = `${this.baseUrl}/rest/app-chat/conversations/new`;
this.isInitialized = false; this.isInitialized = false;
// 使用全局转换器实例,确保与适配器层使用的是同一个实例,从而共享 SSO token 和基础 URL 配置 // 使用全局转换器实例,确保与适配器层使用的是同一个实例
this.converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GROK); this.converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GROK);
if (this.converter) { if (this.converter && this.uuid) {
this.converter.setSsoToken(this.token); this.converter.setUuid(this.uuid);
} }
this.lastSyncAt = null; this.lastSyncAt = null;
@ -520,8 +520,8 @@ export class GrokApiService {
if (resp.rolloutId) collected.rolloutId = resp.rolloutId; if (resp.rolloutId) collected.rolloutId = resp.rolloutId;
// 同步私有字段到最终结果 // 同步私有字段到最终结果
if (resp._ssoToken) collected._ssoToken = resp._ssoToken;
if (resp._requestBaseUrl) collected._requestBaseUrl = resp._requestBaseUrl; if (resp._requestBaseUrl) collected._requestBaseUrl = resp._requestBaseUrl;
if (resp._uuid) collected._uuid = resp._uuid;
if (resp.modelResponse) collected.modelResponse = resp.modelResponse; if (resp.modelResponse) collected.modelResponse = resp.modelResponse;
if (resp.cardAttachment) collected.cardAttachment = resp.cardAttachment; if (resp.cardAttachment) collected.cardAttachment = resp.cardAttachment;
@ -595,9 +595,11 @@ export class GrokApiService {
} }
async * generateContentStream(model, requestBody) { async * generateContentStream(model, requestBody) {
// 确保全局转换器拥有最新的 SSO token 和基础 URL // 确保全局转换器拥有最新的基础 URL 和 UUID
if (this.converter) { if (this.converter) {
this.converter.setSsoToken(this.token); if (this.uuid) {
this.converter.setUuid(this.uuid);
}
if (requestBody._requestBaseUrl) { if (requestBody._requestBaseUrl) {
this.converter.setRequestBaseUrl(requestBody._requestBaseUrl); this.converter.setRequestBaseUrl(requestBody._requestBaseUrl);
} }
@ -705,9 +707,9 @@ export class GrokApiService {
try { try {
const json = JSON.parse(dataStr); const json = JSON.parse(dataStr);
if (json.result?.response) { if (json.result?.response) {
// 注入 SSO token 和基础 URL以便全局转换器能获取到 // 注入基础 URL 和 UUID以便全局转换器能获取到
json.result.response._ssoToken = this.token;
json.result.response._requestBaseUrl = requestBody._requestBaseUrl; json.result.response._requestBaseUrl = requestBody._requestBaseUrl;
json.result.response._uuid = this.uuid;
if (json.result.response.responseId) { if (json.result.response.responseId) {
lastResponseId = json.result.response.responseId; lastResponseId = json.result.response.responseId;
@ -730,8 +732,8 @@ export class GrokApiService {
response: { response: {
isDone: true, isDone: true,
responseId: lastResponseId, responseId: lastResponseId,
_ssoToken: this.token, _requestBaseUrl: requestBody._requestBaseUrl,
_requestBaseUrl: requestBody._requestBaseUrl _uuid: this.uuid
} }
} }
}; };

View file

@ -583,6 +583,20 @@ export class ProviderPoolManager {
return pool?.find(p => p.uuid === uuid) || null; return pool?.find(p => p.uuid === uuid) || null;
} }
/**
* 根据 UUID 在所有池中查找提供商配置
* @param {string} uuid - 提供商 UUID
* @returns {object|null} 提供商配置对象或 null
*/
findProviderByUuid(uuid) {
if (!uuid) return null;
for (const type in this.providerStatus) {
const provider = this.providerStatus[type].find(p => p.uuid === uuid);
if (provider) return provider.config;
}
return null;
}
/** /**
* Initializes the status for each provider in the pools. * Initializes the status for each provider in the pools.
* Initially, all providers are considered healthy and have zero usage. * Initially, all providers are considered healthy and have zero usage.

View file

@ -8,12 +8,14 @@ import { MODEL_PROVIDER } from './common.js';
* @param {http.IncomingMessage} req 原始请求 * @param {http.IncomingMessage} req 原始请求
* @param {http.ServerResponse} res 原始响应 * @param {http.ServerResponse} res 原始响应
* @param {Object} config 全局配置 * @param {Object} config 全局配置
* @param {Object} providerPoolManager 提供商号池管理器
*/ */
export async function handleGrokAssetsProxy(req, res, config) { export async function handleGrokAssetsProxy(req, res, config, providerPoolManager) {
try { try {
const requestUrl = new URL(req.url, `http://${req.headers.host}`); const requestUrl = new URL(req.url, `http://${req.headers.host}`);
const targetUrl = requestUrl.searchParams.get('url'); const targetUrl = requestUrl.searchParams.get('url');
let ssoToken = requestUrl.searchParams.get('sso'); let ssoToken = requestUrl.searchParams.get('sso');
const uuid = requestUrl.searchParams.get('uuid');
if (!targetUrl) { if (!targetUrl) {
res.writeHead(400, { 'Content-Type': 'application/json' }); res.writeHead(400, { 'Content-Type': 'application/json' });
@ -21,9 +23,20 @@ export async function handleGrokAssetsProxy(req, res, config) {
return; return;
} }
// 优先尝试从 uuid 换取 token提高安全性
if (!ssoToken && uuid && providerPoolManager) {
const providerConfig = providerPoolManager.findProviderByUuid(uuid);
if (providerConfig) {
ssoToken = providerConfig.GROK_COOKIE_TOKEN;
logger.debug(`[Grok Proxy] Resolved SSO token from uuid: ${uuid}`);
} else {
logger.warn(`[Grok Proxy] Could not find provider configuration for uuid: ${uuid}`);
}
}
if (!ssoToken) { if (!ssoToken) {
res.writeHead(400, { 'Content-Type': 'application/json' }); res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Missing sso parameter' })); res.end(JSON.stringify({ error: 'Missing sso parameter or valid uuid' }));
return; return;
} }