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:
parent
c91d2ce3ab
commit
3f8fdc0b8e
6 changed files with 77 additions and 58 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
2.10.2
|
||||
2.10.2.1
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
|
|||
* 实现Grok协议到其他协议的转换
|
||||
*/
|
||||
export class GrokConverter extends BaseConverter {
|
||||
// 静态属性,确保所有实例共享最新的认证和基础 URL 配置
|
||||
static sharedSsoToken = null;
|
||||
// 静态属性,确保所有实例共享最新的基础 URL 和 UUID 配置
|
||||
static sharedRequestBaseUrl = "";
|
||||
static sharedUuid = null;
|
||||
|
||||
constructor() {
|
||||
super('grok');
|
||||
|
|
@ -23,20 +23,6 @@ export class GrokConverter extends BaseConverter {
|
|||
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
|
||||
*/
|
||||
|
|
@ -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) {
|
||||
const ssoToken = state?.ssoToken || GrokConverter.sharedSsoToken;
|
||||
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 域名或相对路径
|
||||
const isGrokAsset = url.includes('assets.grok.com') || (!url.startsWith('http') && !url.startsWith('data:'));
|
||||
|
|
@ -67,7 +62,10 @@ export class GrokConverter extends BaseConverter {
|
|||
}
|
||||
|
||||
// 返回本地代理接口 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) {
|
||||
return `${requestBaseUrl}${proxyPath}`;
|
||||
}
|
||||
|
|
@ -78,8 +76,8 @@ export class GrokConverter extends BaseConverter {
|
|||
* 在文本中查找并替换所有 assets.grok.com 的资源链接为绝对代理链接
|
||||
*/
|
||||
_processGrokAssetsInText(text, state = null) {
|
||||
const ssoToken = state?.ssoToken || GrokConverter.sharedSsoToken;
|
||||
if (!text || !ssoToken) return text;
|
||||
const uuid = state?.uuid || GrokConverter.sharedUuid;
|
||||
if (!text || !uuid) return text;
|
||||
|
||||
// 更宽松的正则匹配 assets.grok.com 的 URL
|
||||
const grokUrlRegex = /https?:\/\/assets\.grok\.com\/[^\s\)\"\'\>]+/g;
|
||||
|
|
@ -106,8 +104,8 @@ export class GrokConverter extends BaseConverter {
|
|||
has_tool_call: false,
|
||||
rollout_id: "",
|
||||
in_tool_call: false, // 是否处于 <tool_call> 块内
|
||||
ssoToken: null,
|
||||
requestBaseUrl: "",
|
||||
uuid: null,
|
||||
pending_text_buffer: "" // 用于处理流式输出中被截断的 URL
|
||||
});
|
||||
}
|
||||
|
|
@ -345,32 +343,32 @@ export class GrokConverter extends BaseConverter {
|
|||
/**
|
||||
* 渲染图片为 Markdown
|
||||
*/
|
||||
_renderImage(url, imageId = "image") {
|
||||
_renderImage(url, imageId = "image", state = null) {
|
||||
let finalUrl = url;
|
||||
if (!url.startsWith('http')) {
|
||||
finalUrl = `https://assets.grok.com${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
}
|
||||
finalUrl = this._appendSsoToken(finalUrl);
|
||||
finalUrl = this._appendSsoToken(finalUrl, state);
|
||||
return ``;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染视频为 Markdown/HTML (render_video)
|
||||
*/
|
||||
_renderVideo(videoUrl, thumbnailImageUrl = "") {
|
||||
_renderVideo(videoUrl, thumbnailImageUrl = "", state = null) {
|
||||
let finalVideoUrl = videoUrl;
|
||||
if (!videoUrl.startsWith('http')) {
|
||||
finalVideoUrl = `https://assets.grok.com${videoUrl.startsWith('/') ? '' : '/'}${videoUrl}`;
|
||||
}
|
||||
finalVideoUrl = this._appendSsoToken(finalVideoUrl);
|
||||
finalVideoUrl = this._appendSsoToken(finalVideoUrl, state);
|
||||
|
||||
let finalThumbUrl = thumbnailImageUrl;
|
||||
if (thumbnailImageUrl && !thumbnailImageUrl.startsWith('http')) {
|
||||
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[](${finalVideoUrl})\n[Play Video](${finalVideoUrl})\n`;
|
||||
}
|
||||
|
||||
|
|
@ -455,16 +453,12 @@ export class GrokConverter extends BaseConverter {
|
|||
const modelHash = grokResponse.llmInfo?.modelHash || "";
|
||||
|
||||
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) {
|
||||
state.requestBaseUrl = grokResponse._requestBaseUrl;
|
||||
}
|
||||
if (grokResponse._uuid) {
|
||||
state.uuid = grokResponse._uuid;
|
||||
}
|
||||
|
||||
// 过滤内容并处理其中的 Grok 资源链接
|
||||
content = this._filterToken(content, responseId);
|
||||
|
|
@ -534,17 +528,13 @@ export class GrokConverter extends BaseConverter {
|
|||
const responseId = this._formatResponseId(rawResponseId);
|
||||
const state = this._getState(responseId);
|
||||
|
||||
// 从响应块中同步 token 和基础 URL
|
||||
if (resp._ssoToken) {
|
||||
let processedToken = resp._ssoToken;
|
||||
if (processedToken.startsWith("sso=")) {
|
||||
processedToken = processedToken.substring(4);
|
||||
}
|
||||
state.ssoToken = processedToken;
|
||||
}
|
||||
// 从响应块中同步 uuid 和基础 URL
|
||||
if (resp._requestBaseUrl) {
|
||||
state.requestBaseUrl = resp._requestBaseUrl;
|
||||
}
|
||||
if (resp._uuid) {
|
||||
state.uuid = resp._uuid;
|
||||
}
|
||||
|
||||
if (resp.llmInfo?.modelHash && !state.fingerprint) {
|
||||
state.fingerprint = resp.llmInfo.modelHash;
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export function createRequestHandler(config, providerPoolManager) {
|
|||
|
||||
// Grok assets proxy endpoint
|
||||
if (method === 'GET' && path === '/api/grok/assets') {
|
||||
await handleGrokAssetsProxy(req, res, currentConfig);
|
||||
await handleGrokAssetsProxy(req, res, currentConfig, providerPoolManager);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,10 +108,10 @@ export class GrokApiService {
|
|||
this.chatApi = `${this.baseUrl}/rest/app-chat/conversations/new`;
|
||||
this.isInitialized = false;
|
||||
|
||||
// 使用全局转换器实例,确保与适配器层使用的是同一个实例,从而共享 SSO token 和基础 URL 配置
|
||||
// 使用全局转换器实例,确保与适配器层使用的是同一个实例
|
||||
this.converter = ConverterFactory.getConverter(MODEL_PROTOCOL_PREFIX.GROK);
|
||||
if (this.converter) {
|
||||
this.converter.setSsoToken(this.token);
|
||||
if (this.converter && this.uuid) {
|
||||
this.converter.setUuid(this.uuid);
|
||||
}
|
||||
|
||||
this.lastSyncAt = null;
|
||||
|
|
@ -520,8 +520,8 @@ export class GrokApiService {
|
|||
if (resp.rolloutId) collected.rolloutId = resp.rolloutId;
|
||||
|
||||
// 同步私有字段到最终结果
|
||||
if (resp._ssoToken) collected._ssoToken = resp._ssoToken;
|
||||
if (resp._requestBaseUrl) collected._requestBaseUrl = resp._requestBaseUrl;
|
||||
if (resp._uuid) collected._uuid = resp._uuid;
|
||||
|
||||
if (resp.modelResponse) collected.modelResponse = resp.modelResponse;
|
||||
if (resp.cardAttachment) collected.cardAttachment = resp.cardAttachment;
|
||||
|
|
@ -595,9 +595,11 @@ export class GrokApiService {
|
|||
}
|
||||
|
||||
async * generateContentStream(model, requestBody) {
|
||||
// 确保全局转换器拥有最新的 SSO token 和基础 URL
|
||||
// 确保全局转换器拥有最新的基础 URL 和 UUID
|
||||
if (this.converter) {
|
||||
this.converter.setSsoToken(this.token);
|
||||
if (this.uuid) {
|
||||
this.converter.setUuid(this.uuid);
|
||||
}
|
||||
if (requestBody._requestBaseUrl) {
|
||||
this.converter.setRequestBaseUrl(requestBody._requestBaseUrl);
|
||||
}
|
||||
|
|
@ -705,9 +707,9 @@ export class GrokApiService {
|
|||
try {
|
||||
const json = JSON.parse(dataStr);
|
||||
if (json.result?.response) {
|
||||
// 注入 SSO token 和基础 URL,以便全局转换器能获取到
|
||||
json.result.response._ssoToken = this.token;
|
||||
// 注入基础 URL 和 UUID,以便全局转换器能获取到
|
||||
json.result.response._requestBaseUrl = requestBody._requestBaseUrl;
|
||||
json.result.response._uuid = this.uuid;
|
||||
|
||||
if (json.result.response.responseId) {
|
||||
lastResponseId = json.result.response.responseId;
|
||||
|
|
@ -730,8 +732,8 @@ export class GrokApiService {
|
|||
response: {
|
||||
isDone: true,
|
||||
responseId: lastResponseId,
|
||||
_ssoToken: this.token,
|
||||
_requestBaseUrl: requestBody._requestBaseUrl
|
||||
_requestBaseUrl: requestBody._requestBaseUrl,
|
||||
_uuid: this.uuid
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -583,6 +583,20 @@ export class ProviderPoolManager {
|
|||
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.
|
||||
* Initially, all providers are considered healthy and have zero usage.
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import { MODEL_PROVIDER } from './common.js';
|
|||
* @param {http.IncomingMessage} req 原始请求
|
||||
* @param {http.ServerResponse} res 原始响应
|
||||
* @param {Object} config 全局配置
|
||||
* @param {Object} providerPoolManager 提供商号池管理器
|
||||
*/
|
||||
export async function handleGrokAssetsProxy(req, res, config) {
|
||||
export async function handleGrokAssetsProxy(req, res, config, providerPoolManager) {
|
||||
try {
|
||||
const requestUrl = new URL(req.url, `http://${req.headers.host}`);
|
||||
const targetUrl = requestUrl.searchParams.get('url');
|
||||
let ssoToken = requestUrl.searchParams.get('sso');
|
||||
const uuid = requestUrl.searchParams.get('uuid');
|
||||
|
||||
if (!targetUrl) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
|
|
@ -21,9 +23,20 @@ export async function handleGrokAssetsProxy(req, res, config) {
|
|||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue